第三十九条:必要时进行保护性拷贝
使Java使用起来如此舒适的一个因素在于,它是一门安全的语言。这意味着,它对于缓冲区溢出,数组越界,非法指针以及其他的内存破坏
都自动免疫,而这些错误却困扰着诸如C和C++这样的不安全语言。在一门安全的语言中,在设计类的时候,可以确切的知道,无论系统的
其他部分发生什么事情,这些类的约束都可以保持为真。对于那些“把所有的内存当成一个巨大的数组来看待”的语言来说,这是不可能的。
有一种情况进行保护性拷贝就是需要的:
public class Period
{
private final Date startTime;
private finale Date endTime;
public Period(Date startTime , Date endTime)
{
if(startTime.compareTo(endTime) > 0)
{ throw new IllegalArgumentException(“startTime after endTime !”); }
this.startTime = startTime;
this.endTime = endTime;
}
pubilc Date start()
{ return this.startTime ; }
public Date end()
{ return this.endTime ; }
}
这个类貌似是一个不可变类 ,因为startTime和endTime域都是final的,但是它并不是一个严格的不可变类,因为Date类并不是一个不可变类。
如果客户端这样使用我们定义的Period类 :
Date startTime = new Date();
Date endTime = new Date();
Period per = new Period(startTime , endTime );
endTime.setYear(78);
本来我们希望Period是一个不可变类,只有初始化的那一种状态,但是由于Date类是一个可变类,我们获取了final Date startTime域所引用
的那个对象,而这个对象是一个Date类,我们就可以改变这个对象的状态,从而导致Period的实例对象中的状态进行了改变。
这个时候我们对于构造器进行保护性拷贝 :
public Period(Date startTime , Date endTime )
{
this.startTime = new Date (startTime.getTime());
this.endTime = new Date(endTime.getTime());
if(this.startTime.compareTo(this.endTime) > 0)
{
throw new IllegalArgumentException(“startTime after endTime !”);
}
}
这样在使用上面那样的客户端代码时,在对endTime对象改变状态时,就不会影响到Period类的实例对象状态了。
但是如果客户端使用这样的代码 per.start().setYear(78); 这样同样破坏了per对象的状态。
那么对于返回实例对象域的start(),end()方法也应该使用保护性拷贝。
public Date start()
{
return new Date(startTime.getTime());
}
public Date end()
{
return new Date(endTime.getTime());
}
这样,我们定义的Period类就真正变成了一个不可变类,它的实例对象只有在初始化的那一种状态。
使用保护性拷贝是为了我们的代码更加健壮,考虑客户端代码的种种恶意破坏。
一般情况,当我们想要定义一个不可变类,但是这个类的域的类型确实一个可变类,那么我们在定义构造器的时候是需要进行保护性拷贝的。
而且记住对于参数有效性的检验应该在保护性拷贝之后。