提示五十:必要时进行保护性拷贝。
愉快使用 Java 的原因,它是一种安全的语言。 这意味着在缺少本地方法的情况下,它不受缓冲区溢出,数组溢出,野指针以及其他困扰 C 和 C++ 等不安全语言的内存损坏错误的影响。 在一种安全的语言中,无论系统的任何其他部分发生什么,都可以编写类并确切地知道它们的不变量会保持不变。 在将所有内存视为一个巨大数组的语言中,这是不可能的。
// Broken "immutable" time period class
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;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
... // Remainder omitted
}
书中给出了这样一个例子,看上去start,end都是final类型的变量,在构造函数的时候也做了校验,应该没有什么问题。但是因为 Date 类本身是可变的,因此很容易违反这个约束条件。从 Java 8 开始,解决此问题的显而易见的方法是使用 Instant(或 LocalDateTime 或 ZonedDateTime)代替 Date,因为 Instant(和其他 java.time 类)是不可变的。
但是老代码依然还在那里,甚至已经被大量使用。为了避免这种问题,可以对于构造器的每个可变参数进行保护性拷贝(defensive copy)并且使用备份对象作为 Period 实例的组件,而不使用原始的对象。
// Repaired constructor - makes defensive copies of parameters
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);
}
// Repaired constructor - makes defensive copies of parameters
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);
}
注意,保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始的对象。并且对于参数类型可以被不可信任方子类化的参数,不要使用 clone 方法进行保护性拷贝。
我们系统中其实也大量存在这样的问题,主要平时开发的时候没有这种思维,不会想着时刻保护自己的代码。Guava中有着一套ImmutableCollection,并且有着很好用的copy工具,如果想要保护参数应该很好用。