绝大多数方法和构造器对于传递给它们的参数值都会有些限制。比如,索引值必须大于等于0,且不能超过其最大值,对象不能为null等。这样就可以在导致错误的源头将错误捕获,从而避免了该错误被延续到今后的某一时刻再被引发,这样就是加大了错误追查的难度。就如同编译期能够报出的错误总比在运行时才发现要更好一些。事实上,我们不仅仅需要在函数的内部开始出进行这些通用的参数有效性检查,还需要在函数的文档中给予明确的说明,如在参数非法的情况下,会抛出那些异常,或导致函数返回哪些错误值等,见如下代码示例:
/**
* Returns a BigInteger whose value is(this mod m). This method
* differs from the remainder method in that it always returns a
* non-negative BigInteger.
* @param m the modulus, which must be positive.
* @return this mod m.
* @throws ArithmeticException if m is less than or equal to 0.
*/
public BigInteger mod(BigInteger m) {
if (m.signum() = 0 && offset = 0 && length 0) {
throw new IllegalArgumentException(start + "After " + end);
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
}
从表面上看,该类的实现确实对约束性的条件进行了验证,然而由于Date类本身是可变了,因此很容易违反这个约束,见如下代码:
public void testPeriod() {
Date start = new Date();
Date end = new Date();
Period p = new Period(start,end);
end.setYear(78); //该修改将直接影响Period内部的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);
}
需要说明的是,保护性拷贝是在坚持参数有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始对象的。这主要是为了避免在this.start = new Date(start.getTime())到if (start.compareTo(end) > 0)这个时间窗口内,参数start和end可能会被其他线程修改。
现在构造函数已经安全了,后面我们需要用同样的方式继续修改另外两个对象访问函数。
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
经过这一番修改之后,Period成为了不可变类,其内部的“周期的起始时间不能落后于结束时间”约束条件也不会再被破坏。
参数的保护性拷贝并不仅仅针对不可变类。每当编写方法或者构造器时,如果它要允许客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象是否有可能是可变的。如果是,就要考虑你的类是否能够容忍对象进入数据结构之后发生变化。如果答案是否定的,就必须对该对象进行保护性拷贝,并且让拷贝之后的对象而不是原始对象进入到数据结构中。
例如,如果你正在考虑使用有客户提供的对象引用作为内部Set实例的元素,或者作为内部Map实例的键(Key),就应该意识到,如果这个对象在插入之后再被修改,Set或者Map的约束条件就会遭到破坏。
第40条:谨慎设计方法签名- 谨慎地选择方法的名称
- 避免过长的参数列表,目标是四个参数或者更少,如果多于四个了就该考虑重构这个方法了(把方法分解多个小方法、创建辅助类、从对象构建到方法调用都采用Builder模式)。
- 对于参数类型、要优先使用接口而不是类。如果使用的是类而不是接口,则限制了客户端只能传入特定的实现,如果碰巧输入的数据是以其他的形式存在,就会导致不必要的、可能非常昂贵的拷贝操作。
- 对于boolean参数,优先使用两个元素的枚举类型。
下面的例子根据一个集合是Set、List还是其他的集合类型,来对它进行分类:
public class CollectionClassfier {
public static String classify(Set s) {
return "Set";
}
public static String classify(List l) {
return "List";
}
public static String classify(Collection c) {
return "Unknown collection";
}
public static void main(String[] args) {
Collection[] collections = {new HashSet(), new ArrayList(), new HashMap().values()};
for (Collection c : collections)
System.out.println(classify(c));
}
}
这里你可能会期望程序打印出Set、List、Unknown Collection,然而实际上却不是这样,输出的结果是3 个”Unknown Collection”。 因为classify方法被重载了,需要调用哪个函数是在编译期决定的,for中的三次迭代参数的编译类型是相同的:
Collection,对于这种情况,int 和Collection之间没有任何关联,也无法在两者之间做任何的类型转换,否则将会抛出ClassCastException 的异常,因此对于这种函数重载,我们是可以准确确定的。反之,如果两个参数分别是int 和short,他们之间的差异就不是这么明显。 二、如果方法使用可变参数,保守的策略是根本不要重载它。 三、对于构造器,你没有选择使用不同名称的机会,一个类的多个构造器总是重载的,但是构造器也不可能被覆盖。 四、在Java 1.5 之后,需要对自动装箱机制保持警惕。 演示如下:
public class SetList {
public static void main(String[] args) {
Set s = new TreeSet();
List l = new ArrayList();
for (int i = -3; i < 3; ++i) {
s.add(i);
l.add(i);
}
for (int i = 0; i < 3; ++i) {
s.remove(i);
l.remove(i);
}
System.out.println(s + " " + l);
}
}
在执行该段代码前,我们期望的结果是Set 和List 集合中大于等于的元素均被移除出容器,然而在执行后却发现事实并非如此,其结果为:[-3,-2,-1] [-2,0,2]。这个结果和我们的期望还是有很大差异的,为什么Set 中的元素是正确的,而List 则不是,是什么导致了这一结果的发生呢?
下面给出具体的解释:
s.remove(i)调用的是Set 中的remove(E),这里的E 表示Integer,Java 的编译器会将i 自动装箱到Integer 中,因此我们得到了想要的结果。
l.remove(i)实际调用的是List 中的remove(int index)重载方法,而该方法的行为是删除集合中指定索引的元素。这里分别对应第0 个,第1 个和第2 个。
为了解决这个问题,我们需要让List 明确的知道,我们需要调用的是remove(E)重载函数,而不是其他的,这样我们就需要对原有代码进行如下的修改:
public class SetList {
public static void main(String[] args) {
Set s = new TreeSet();
List l = new ArrayList();
for (int i = -3; i < 3; ++i) {
s.add(i);
l.add(i);
}
for (int i = 0; i < 3; ++i) {
s.remove(i);
l.remove((Integer)i); //or remove(Integer.valueOf(i));
}
System.out.println(s + " " + l);
}
}
总结,对于多个具有相同参数数目的方法来说,应该尽量避免重载方法。我们应当保证:当传递同样的参数时,所有重载方法的行为必须一致。
第42条:慎用可变参数
可变数组机制是通过先创建一个数组,数组的大小为在调用位置所传递的参数数量,然后将参数值传到数组中,最后将数组传递给方法。
有的时候在重视性能的情况下,使用可变参数机制要特别小心。可变参数方法的每次调用都会导致进行一次数组分配和初始化。如果确定确实无法承受这一成本,但又需要可变参数的灵活性,还有一种模式可以弥补这一不足。假设确定对某个方法95%的调用会有3个或者更少的参数,就声明该方法的5个重载,每个重载方法带有0个至3个普通参数,当参数的数目超过3个时,就使用一个可变参数方法:
public void foo() {}
public void foo(int a1) {}
public void foo(int a1,int a2) {}
public void foo(int a1,int a2,int a3) {}
public void foo(int a1,int a2,int a3,int...rest) {}
所有调用中只有5%参数数量超过3个的调用需要创建数组。就像大多数的性能优化一样,这种方法通常不恰当,但是一旦真正需要它时,还是非常有用处的。
在定义参数数目不定的方法时,可变参数方法是一种很方便的方式,但是它们不应该过度滥用。如果使用不当,会产生混乱的结果。
第43条:返回零长度的数组或者集合,而不是null
有时候会有人认为:null返回值比零长度数据更好,因为它避免了分配数组所需要的开销。 这种观点是站不住脚的,原因有两点。
- 在这个级别上担心性能问题是不明智的,除非分析表明这个方法正是造成性能问题的真正源头。
- 对于不返回任何元素的调用,每次都返回同一个零长度数组是有可能的,因为零长度数组是不可变的,而不可变对象有可能被自由地共享。
private static final Cheese[] EMPTY_CHEESE_ARRAY= new Cheese[0];
相比于数组,集合亦是如此。 在Collections中有专门针对List,Set,Map的空的实现。如:
Collections.emptyList()
Collections.emptySet();
Collections.emptyMap();
第44条:为所有导出的API元素编写文档注释
略
《Effective Java中文版 第2版》PDF版下载: http://download.csdn.net/detail/xunzaosiyecao/9745699
作者:jiankunking 出处:http://blog.csdn.net/jiankunking