JAVA是一门安全的语言。这就意味着,它对缓冲区溢出、数组越界、非法指针以及其它的内存破坏错误都自动免疫,而这些错误却困扰着诸如C和C++这样的不安全的语言。在一门安全的语言中,可以确切的知道,无论系统的其他部分发生什么事,这些类的约束都可以保持为真。对于那些“把内存当做一个巨大数组看待”的语言来说,这是不可能的。
假设类的客户端会尽其所能来破坏这个类的约束条件,因此你必须保护性的设计程序。考虑下列类,它声称可以表示一段不可变的时间周期:
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;
}
//remainder omitted
}
这个类看上去没有什么问题,时间是不可改变的。然而Date类本身是可变的,因此很容易违反这个约束条件。
//Attack the internals of a Period instance
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
end.setYear(78);
System.out.println(period.end());
为了保护Period实例的内部信息避免受到修改,导致问题,对于构造器的每个可变参数进行保护性拷贝(defensive copy)是必要的:
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(this.start + " after " + this.end);
}
}
用了新构造器之后,上面的攻击对于Period实例不再有效。保护性拷贝是在检查参数合法性之前进行的,而且参数的合法性的检查是针对保护性拷贝之后的对象,而不是针对原始对象。这样做是为了避免危险阶段来自另外一个线程的修改参数。危险阶段指的是检查参数开始,直至保护性拷贝参数之间的时间。在计算机安全社区中,这段时间被称为TOC、TOU(Time-Of-Use,Time-Of-Check)。
对于参数类型可以被不可信任方子类化的参数,请不要使用clone方法进行保护性拷贝。
虽然替换构造器就可以成功地避免上述的攻击,但是改变Period实例仍然是有可能的,因为它的访问方法提供了对其可变内部成员的访问能力:
//Second attack on the internals of a Period instance
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
end.setYear(78);
System.out.println(period.end());
为了防止二次攻击,可以让end()返回拷贝对象。
public Date end(){
return new Date(end.getTime());
}
采用了新的构造器和新的访问方法之后,Period真正是不可变的了。
参数的保护性拷贝不仅仅针对不可变类。每当编写方法和构造器时,如果他要允许客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象是否有可能是可变的,我是否能够容忍这种可变性。特别是你用到list、map之类连接元素时。
如果类具有从客户端得到或者返回到客户端的可变组件,类就必须保护性的拷贝这些组件。如果拷贝的成本受到限制,并且类信任他的客户端不会进行修改,或者恰当的修改,那么就需要在文档中指明客户端调用者的责任(不的修改或者如何有效修改)。特别是当你的可变组件的生命周期很长,或者会多层传递时,隐藏的问题往往暴漏出来就很可怕。
当我们准备实现一个具有特殊约束条件类的时候,假设类的客户端会尽其所能的破坏这个类的约束条件,因此我们必须保护性设计程序。