Effective Java(三)

来源:互联网 发布:linux 查看session 编辑:程序博客网 时间:2024/06/11 09:24
本次主要介绍Effective Java中"方法"和"通用程序设计"这两章的内容。
一.检查参数的有效性
这点内容比较好理解,我们平时的开发中要对方法中的参数合法性进行判断,在大多数情况下我们都要假设参数是非法的,存在攻击行为的,这样才能保证我们的程序更健壮。如果传递无效的参数值给方法,这个方法在执行之前先对参数进行了检查,那么它很快就会失败,并且清楚地出现适当的异常(我们平时开发的时候一般返回null)。如果这个方法没有检查它的参数,就有可能发生几种情形。方法在处理过程中失败,并且产生令人费解的异常。更糟糕的是该方法可以正常返回,但是会悄悄地计算出错误的结果。最糟糕的是,方法可以正常返回,但是却使得某个对象处于被破坏的状态,在某个不确定的时候引发错误。此外我们可以在javadoc上说明违反参数限制时会抛出的异常。
二.必要时进行保护性拷贝
从标题上有点不知所云,我们用一个例子来说明保护性拷贝。
public final class Period {     private final Date start ;     private final Date end ;     public Period(Date start,Date end){           if(start .compareTo(end) > 0){               throw new IllegalArgumentException(start+" after "+end);          }           this.start = start ;           this.end = end ;     }     public Date start(){           return start ;     }     public Date end(){           return end ;     }}
乍一看,这个类似乎是不可变的,并且强加了约束条件:周期的起始时间不能再结束时间之后。然而,因为Date类本身是可变的,因此很容易违反这个约束条件,如下所示:        
 Date start = new Date();          Date end = new Date();          Period p = new Period(start ,end );          end.setYear(78);
这样就破坏了start < end的规则,为了保护Period实例的内部信息避免受到这种攻击,对于构造器的每个可变参数进行保护性拷贝是必要的。改进如下:
    
 public Period(Date start,Date end){           this.start = new Date(start.getTime());           this.end = new Date(end.getTime());           if(start .compareTo(end) > 0){               throw new IllegalArgumentException(start+" after "+end);          }     }
保护性拷贝就是不通过参数对象直接赋值,而是拷贝一个对象来赋值。注意,保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始的对象。这样做可以避免在"危险阶段"期间从另一个线程改变类的参数,这里的危险阶段是指从检查参数开始到拷贝参数之间的时间段。我们没有用Date的clone方法来进行保护性拷贝,是因为Date是非final的,不能保证clone方法一定返回类为java.util.Date的对象。
同时我们也需要避免上面的start()方法和end()方法被攻击。修改为:
     
 public Date start(){           return new Date(start .getTime());     }     public Date end(){           return new Date(end .getTime());     }
参数的保护性拷贝并不仅仅针对不可变类。每当编写方法或者构造器时,如果它要允许客户提供的对象进入到内部数据结构时,则有必要考虑一下,客户提供的对象是否有可能是可变的,如果是,就要考虑你的类是否能够容忍对象进入数据结构之后发生变化。只要有可能,都应该使用不可变的对象作为对象内部的组件,这样就不必子再为保护性拷贝操心。在Period中,有经验的程序员通常使用Date.getTime()返回的long基本类型作为内部的时间表示法,而不是使用Date,主要因为Date是可变的。
其实书中一直强调尽量使用不可变类,不过我平时的工作中很少见到,看来我们的系统还不够健壮。
三.谨慎设计方法签名
这条主要说的是一些规范和方法改进和一些API设计技巧的总结。
1.谨慎第选择方法的名称。这点很好理解,我们的代码是给别人看的,应该让别人从名字上就知道这个方法的作用。
2.不要过于追求提供便利的方法。每个方法都应该尽其所能,方法太多会使类难以学习,使用,测试和维护。只有当一项操作被经常用到的时候,才考虑提供快捷方式,如果不确定,还是不提供为好。
3.避免过长的参数列表。方法要尽量保证不超过四个参数,大多数程序员无法记住更长的参数列表,而且用户不小心弄错了参数顺序时,程序仍然可以变异和运行,但是不会按照正常意图进行。
有三种方法可以缩短过长的参数列表。
(1)把方法分解成多个方法,每个方法只需要这些参数的一个子集。(如果不小心会导致方法过多,需要仔细考虑如何设计减少方法数目,如提升它们的正交性,就是尽量让方法通用)
(2)创建辅助类,用来保存参数的分组,这些辅助类一般为静态成员类。
(3)从对象的构建到方法调用都采用Builder模式。
4.对于参数类型,优先使用接口而不是类。如我们使用Map接口作为参数,这样我们可以传入一个Hashtable,HashMap,TreeMap等实现类。如果使用类则先知了客户端智能传入特定的实现。
四.返回零长度的数组或者集合,而不是null
关于这条,工作中还是有不少接口返回null用来代表空的,而前端也是通过是否为null来判断的,所以关于这点还是要设计者和调用方遵守通用约定,明确文档即可,虽然返回null如果直接使用会空指针。
五.将局部变量的作用域最小化
要使局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方声明。如果变量在使用之前进行声明,会对试图理解程序功能的读者造成混乱,等到用到该变量的时候读者可能记不起该变量的类型或初始值了。
几乎每个局部变量的声明都应该包含一个初始化表达式。
六.如果需要精确的答案,避免使用float和double
float和double类型主要是为了科学计算和工程计算而设计的。他们执行二进制浮点运算,是为了在广泛的数值范围上提供较为精确的快速近似计算而精心设计的。然而,它们并没有提供完全精确的结果。
如System.out.println(1.03 - 0.42);结果是0.6100000000000001
解决方法是使用BigDecimal代替double。然而BigDecimal有两个缺点:与使用基本运算类型相比,操作很不方便,而且很慢。除了使用BigDecimal之外,还可以使用int或者long,同事要自己处理十进制小数点。
七.基本类型优先于装箱基本类型
Java的基本类型如int,double等都有其对应的包装类型Integer,Double等。Java1.5版本中增加了自动装箱和自动拆箱功能。
在基本类型和装箱类型之间有三个主要区别。
1.基本类型只有值,而装箱类型则具有与它们的值不同的同一性。换句话说,两个装箱类型可以有相同的值和不同的同一性。
2.基本类型只有功能完备的值,而装箱类型除了它对应基本类型的所有功能值之外,还有个非功能值:null。
3.基本类型通常比装箱类型更节省时间和空间。
这几点区别主要产生的问题为,我们使用装箱类型时用==是会返回false的,装箱类型不进行null判断有时候会抛出NullPointerException异常。
       
 Long sum = 0L;           for(long i = 0;i < Integer.MAX_VALUE; i++){               sum += i;          }          System. out.println(sum );
这段程序因为sum声明为装箱类型,会导致大量的装箱拆箱操作,性能很差。使用基本类型的操作时间本机测试900ms左右,而使用装箱类型操作时间则为11000ms,是基本类型耗时的十几倍。
那么什么时候使用装箱类型呢?他们又几个合理的用处。作为集合中的元素,键和值,集合不支持基本类型。参数化类型中必须使用装箱类型,如不能声明ThreadLocal<int>,必须使用ThreadLocal<Integer>代替。
八.当心字符串连接的性能
字符串链接操作符"+"不适合运用在大规模的场景中。为连接n各字符串而重复的使用字符串连接操作符,需要n的平方级的时间。这是由于字符串不可变而导致的。当两个字符串呗连接在一起时,他们的内容都要被拷贝。
所以在项目数量巨大时,为了提高性能,请使用StringBuilder替代String。附带StringBuilder.append(String str);实现代码:
     
public AbstractStringBuilder append(String str ) {        if (str == null) str = "null";        int len = str .length();        ensureCapacityInternal( count + len);        str.getChars(0, len, value, count);        count += len;        return this ;    }
九.谨慎地进行优化
工作中,我们经常会进行各种各样的优化,重构,而这条建议说谨慎地进行优化,甚至说不要进行优化,比较有意思。主要是因为如果我们没有合理的优化方案的时候,优化很多时候会让我们的代码运行的更慢。在每次试图做优化之前和之后,要对性能进行测量。有时候你会发现,试图做的优化通常对于性能并没有明显的影响,有时候甚至会使性能变得更差。
0 0