我们来看一个不可变对象的攻守问题:
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类并不是一个不可变类,所以Date实例指向内存中的引用地址不可变,但是引用的内容可以变,所以可以这样来攻击不可变类
Date startTime = new Date();
Date endTime = new Date();
Period per = new Period(startTime , endTime );
endTime.setYear(78);
这个时候我们对于构造器进行保护性拷贝
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 !”);
}
}
也就是说把这个可变的Date引用指向了一个拷贝,这样endTime.setYear(78)永远也修改不了这个拷贝。
值得注意的是:保护性拷贝是在检查参数有效性之前进行的,而且针对的是拷贝对象。为什么呢?因为担心并发情况下检查有效性通过之后,另一个线程改变了可变对象使其有效性错误,但是依然能进行保护性拷贝的错误情况。
同时我们没有用Date的clone方法进行保护性拷贝。为什么呢?
因为Date是非final的,不能保证clone方法返回的就是Date对象,它有可能返回一个专门出于恶意目的而设计的不可信子类的实例。
例如:上面例子中如果别有用心之人新建了一个Mydate继承了Date
那么构造方法中就可以传入MyDate对象,那么调用的就是MyDate的clone方法,那么他就可以在clone方法上动手脚,把实例的引入记录起来,供攻击者访问。
还有一种攻击方法:
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());
}
总结:参数的保护性拷贝策略不仅仅是针对不可变类,如果类具有从客户端得到或者返回到客户端的可变组件,类必须保护性地拷贝这些组件。如果受到拷贝成本的约束,就应该明确向客户端指明。
避免保护性拷贝的方法是将对象组件设置为不可变组件。或者把客户端和类放在同一个包中,不暴露出去。