第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()返回的基本类型作为内部的时间表示法,就不需要进行保护性拷贝了。

 

如果类具有从客户端得到或者返回客户端的可变组件,类就必须保护性地拷贝这些组件,如果拷贝成本受到限制,并且类信任调用者不会修改内部组件,不进行保护性拷贝也是可以的,类的文档必须清楚说明这一点。

posted @ 2016-08-06 13:23  没有梦想的小灰灰  阅读(395)  评论(0编辑  收藏  举报