第39条:必要时进行保护性拷贝
Java是一门安全的语言,但是如果不采取措施,还是无法保证安全性。假设类的客户端会尽其所能来破坏类的约束条件,因此必须保护性地设计程序。
考虑下面的类,声称表示一段不可变的时间周期:
import java.util.Date; 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; } }
因为Date本身是可变的,因此很容易违反起始时间不能在结束时间之后的约束。
Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); end.setYear(78);//改变了终止时间,破坏类的约束条件
为保护Period实例的内部信息被破坏,对构造器的每个可变参数进行保护性拷贝,使用备份对象作为Period实例的组件,而不使用客户端传入的参数。
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); }
保护性拷贝在参数有效性检查之前,这样做可以避免在多线程环境中,参数有效性通过后,从另外一个线程改变参数,使得参数有效性的检测失效。
不使用clone方法进行保护性拷贝的原因是Date是非final的,不能保证clone方法一定返回Date对象,它可能会返回转么处于恶意的不可信子类的实例。
进行了上面的工作后,改变Period实例仍有可能,因为它的访问方法提供了对其可变内部成员的访问能力:
Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); p.end().setYear(78);
改变访问方法,返回可变内部域的保护性拷贝:
public Date start() { return new Date(start.getTime()); } public Date end() { return new Date(end.getTime()); }
采用新构造器和新访问方法后,Period真正地是不可变的了,不管程序员多么恶意和不合格,都绝对不会违反周期起始时间大于结束时间的约束。访问方法与构造器不同,在进行保护性拷贝的时候允许使用clone方法,因为Period内部Date的对象一定是Date,而不可能是某个不可信的子类。
只要有可能,应该使用不可变的对象作为对象内部组件,这样不必为保护性拷贝操心。在Period例子中,使用Date.getTime()返回的基本类型作为内部的时间表示法,就不需要进行保护性拷贝了。
如果类具有从客户端得到或者返回客户端的可变组件,类就必须保护性地拷贝这些组件,如果拷贝成本受到限制,并且类信任调用者不会修改内部组件,不进行保护性拷贝也是可以的,类的文档必须清楚说明这一点。