第38条 检查参数的有效性
1.这是“应该在发生错误之后尽快检测出错误”这一普遍原则的一个具体情形,这可以使发生错误的时候更容易确定错误的根源
2.assert 语句,断言如果失败,将会抛出AssertionError。
在生产环境中,一般是不支持assert的,因此这样可以提高效率,没有成本开销。所以,assert只在私有方法中使用,因为私有方法的调用者开发者,他和被调用者之间是一种弱契约关系,或者说没有契约关系,其间的约束是依靠开发者自己控制的,开发者应该有充分的理由相信自己传入的参数是有效的。所以,从某种角度上来说,assert只是起到一个预防开发者自己出错,或者是程序的无意出错。
3.对于有些参数,方法本身没有用到,却被保存起来以后使用,检查这类参数的有效性尤为重要,因为不检查以后出错更难查到来源
4.构造器就是第3条的特殊情形
5.在有些情况下,有效性检查工作非常昂贵,或者根本是不切实际的,而且有效性检查已隐含在计算过程中完成,如Collection.sort(List).列表中所有对象都必须是可以相互比较的。
6.有时候,某些计算会隐式地执行必要的有效性检查,检查失败抛出的异常与文档中标明的这个方法抛出的异常不相符,这种情况下,应该使用61条所述的异常转译
第39条 必要时进行保护性拷贝
// Broken "immutable" time period class - Page 184
package org.effectivejava.examples.chapter07.item39;
import java.util.Date;
public final class Period {
private final Date start;
private final Date end;
/**
* @param start
* the beginning of the period
* @param end
* the end of the period; must not precede start
* @throws IllegalArgumentException
* if start is after end
* @throws NullPointerException
* if start or end is null
*/
public Period(Date start, Date end) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(start + " after " + end);
this.start = start;
this.end = end;
}
// Repaired constructor - makes defensive copies of parameters - Page 185
// Stops first attack
// public Period(Date start, Date end) {
// this.start = new Date(start.getTime());
// this.end = new Date(end.getTime());
//
// if (this.start.compareTo(this.end) > 0)
// throw new IllegalArgumentException(start +" after "+ end);
// }
public Date start() {
return start;
}
public Date end() {
return end;
}
// Repaired accessors - make defensive copies of internal fields - Page 186
// Stops second attack
// public Date start() {
// return new Date(start.getTime());
// }
//
// public Date end() {
// return new Date(end.getTime());
// }
public String toString() {
return start + " - " + end;
}
// Remainder omitted
}
这个类(不算注释部分)声称是不可变的(immutable),的确start和end都是final,但他们本身是可变的
所以用使用保护性拷贝,用注释部分代替原来的
注意在新的构造器中,保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始的对象
这样做可以避免在“危险阶段(window of vulnerability)”期间从另一个线程改变类的参数,这里的危险阶段是指**从检查参数开始,直到拷贝参数(使用参数)之间的时间段**。(在计算机安全社区中,这被称作Time-Of-Check/Time-Of-Use或这TOCTOU攻击)
也就是如果构造函数是这样的话:
public Period(Date start, Date end) {
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(start +" after "+ end);
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
}
会产生这样的问题:线程A调用Period构造函数,传入有效的(start早于end)的两个date参数,等线程A过了参数检查,然后马上线程B修改start和end使得start晚于end。
同时还要注意这里构造器没有用Date的clone方法来进行保护性拷贝。因为:**对于参数类型可以被不可信任子类化的参数,请不要使用clone方法进行保护性拷贝**。
就是说这里Date是非final的,所以可以被子类化,也就不能保证传进来的是java.util.Date的实例,也就不能保证clone返回的类是java.util.Date的对象,比如万一返回的是一个恶意的子类,他可以在每个实例被创建的时候,把指向该实例的引用记录到一个私有的静态列表中,并且允许攻击者访问这个列表,这使得攻击者可以自由地控制所有的实例。
不过访问方法中Date的保护性拷贝是可以用clone的,因为我们知道Period内部的Date对象就是java.util.Date的实例。
还有一点:有经验的程序员通常使用Date.getTime()返回的long基本类型作为内部的时间表示法,而不是使用Date对象。因为Date可变,而加了final的long不可变
参数的保护性拷贝不仅仅是针对不可变类,每当编写方法或构造器时,如果客户提供的对象将要进入你这个类这个数据结构,都要想一想,客户提供的对象是否可变,或者是否能够容忍它变。如果是可变且不能容忍其变,那就要进行参数的保护性拷贝。
在内部组件返回给客户端之前也同样要想一想。
当然也不必然,如果类信任它的调用者不会修改内部的组件,可能因为类及其客户端都是同一个包的双方,那么不进行保护性拷贝也是可以的。在这种情况下,类的文档中就必须清楚地说明,调用者绝不能修改受到影响的参数或者返回值。
即使跨越包的作用范围,其实也不一定要保护性拷贝。有一些方法和构造器的调用,要求参数所引用的对象必须有个显式的交接(handoff)过程。当客户端调用这样的方法时,它承诺以后不再直接修改该对象。同样这种情况也要在文档中指明。
还有一种情况可以不用保护性拷贝,就是信任客户端,且客户端私自改变该对象不会伤害到除了客户端之外的其他对象时。
第40条 谨慎设计方法签名
1.谨慎地选择方法的名称
2.不要过于追求提供便利的方法
3.避免过长的参数列表。少于等于4个
把方法分解成多个方法.
创建辅助类(一般是静态成员类),用来保存参数的分组.
从对象构建到方法调用都采用Builder模式
4.对于参数类型,要优先使用接口而不是类。
5.对于boolean参数,要优先使用两个元素的枚举类型。(也就是说设计时一个参数只可能有两个可能取值,那第一选择不是把参数类型设为boolean而是两个元素的枚举类型)
第41条 慎用重载
// Broken! - What does this program print?
public class CollectionClassifier {
public static String classify(Set<?> s) {
return "Set";
}
public static String classify(List<?> lst) {
return "List";
}
public static String classify(Collection<?> c) {
return "Unknown Collection";
}
public static void main(String[] args) {
Collection<?>[] collections = { new HashSet<String>(),
new ArrayList<BigInteger>(),
new HashMap<String, String>().values() };
for (Collection<?> c : collections)
System.out.println(classify(c));
}
}
上面的程序会输出什么?答案是3次Unknown Collection。
**要调用哪个重载(overload)方法是在编译时作出决定的**,也就是要看他的**编译时类型**
而for中三次迭代,参数的**编译时**类型都是Collction<?>
对于重载方法(overloaded method)的选择是静态的,而对于被覆盖的方法(overridden method)的选择则是的动态的。(下面是我自己的总结,不一定正确)也就是说方法的“定位”可以这样理解,**编译时**根据方法的参数类型,确定方法的签名,然后在**运行时**根据调用者的类型在类层次上找。
因为覆盖机制是规范,而重载机制是例外,所以覆盖机制满足了人们对于方法调用行为的期望,重载很容易使这些希望落空。
因此应该避免胡乱地使用重载机制。怎样算乱用呢,一个安全而保守的策略是:永远不要导出两个具有相同参数数目的重载方法,如果方法使用可变参数,保守的策略是根本不要重载它。
那替代方案是什么呢?举个例子就知道了:ObjectOutputStream有writeBoolean(boolean),writeInt(int)等方法签名,而不是重载write(int),write(boolean)等。
但构造器呢?毕竟一个类的多个构造器总是重载的。一种方法是导出静态工厂,另一种就是让参数“根本不同(radically different)”
第42条 慎用可变参数
如果只有一个方法只有可变参数,那最大的问题在于如果不传参数调用该方法会在运行时而不是编译时报错。
解决方法是同时声明一个正常类型的参数和可变参数
不必改造具有final数组参数的每个方法只有当确定是在数量不定的值上执行调用时才可以使用可变参数。
在重视性能的情况下,使用可变参数机制要特别小心
第43条 返回零长度的数组或者集合,而不是null
有人认为返回null比返回零长度数组更好,因为可以避免开销,这种观点是站不住脚的:
1.在这个级别上担心性能问题是不明智的
2.每次都返回同一个零长度数组是有可能的,因为零长度数组是不可变的,而不可变对象可能被自由地共享