有这么一个类Period:
public 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 start = new Date();
Date end = new Date();
Period p = new Period(start, end);
start.setYear(66);
为了应对这种修改方式,可以采用在构造时保护性拷贝的办法:
public class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
// 这里对start和end进行保护性拷贝
this.start = new Date(start);
this.end = new Date(end);
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException(start + "after" + end);
}
}
public Date start() {
return start;
}
public Date end() {
return end;
}
}
注意,之前是先判断start和end的有效性,再初始化变量;而现在改为先初始化,再判断有效性,其原因是——先判断有效性则可以在判断之后修改变量,即TOCTOU攻击。在初始化Period时,先传入有效的start和end,过了检查之后以及start和end初始化之前,在另外一个线程修改start和end,使其无效,然后Period的start和end就会出问题但不会被程序所捕捉到。因此要先初始化,再判断有效性。
此时,还有一种修改方法:
Date start = new Date();
Date end = new Date();
Period p = new Date(start, end);
p.end().setYear(66);
所以还应该在getter中加入保护性拷贝:
public class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
// 这里对start和end进行保护性拷贝
this.start = new Date(start);
this.end = new Date(end);
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException(start + "after" + end);
}
}
public Date start() {
return new Date(start);
}
public Date end() {
return new Date(end);
}
}
这就是完善好的Period类了。对于此类,还可以使用Instant等不可变类替代Date。
一般地,如果一个类有可变的组件,那么当从客户端获取和返回组件时,应该使用保护性拷贝。
然而,当对象的拷贝开销很大时,如果客户端不会不恰当地修改类的组件,那么可以考虑不使用保护性拷贝,但一定要在文档中说明。
一方面,程序中要避免创建冗余的对象,以减少资源占用;另一方面,在必须创建对象时,那就去创建,以保证程序的安全性和健壮性。
读《Effective Java》记录一下,留作以后参考。