NormanZyq
发布于 2019-03-30 / 295 阅读
0
0

关于Mutable和Immutable

先看一段代码,这是我最初在设计ConcreteEdgesGraph时写下的代码节选,为了节省空间,我略去了多数方法和所有文档注释

public class ConcreteEdgesGraph<L> implements Graph<L> {

    private final Set<L> vertices = new HashSet<>();
    private final List<Edge<L>> edges = new ArrayList<>();

	private void checkRep() { ... }

	// 注意这两个方法
    public List<Edge<L>> getEdges() {
    	checkRep();
        return this.edges;
    }
    
    @Override
    public Set<L> vertices() {
    	checkRep();
        return this.vertices;
    }
}

乍一看,这两个类都是很普通的类,没有什么安全隐患,getEdges()和vertices()方法都是将final的对象返回,貌似别人也不能修改我的引用——这么想就不对了,因为List和Set以及很多集合类型的特性,它们内部都是mutable(可变)的。

所以,如果一个恶意的客户端在获取了Edges之后随意使用诸如add()remove()的方法,不仅破坏我规定的RI(表示不变性),同时还绕过了我的checkRep()!于是这中间的错误越积越多,导致程序最终无法正确运行。

再举一个例子:
首先假设MyObject类的实现如下:

public class MyObject {
	String name;		// 不可变数据类型
	Date date;		// 可变数据类型
	
	// MyObject构造器
	public MyObject(String name, Date date) {
		this.name = name;
		this.date = date;
	}
}

现在外部有如下方法:

/**
* 创建一个MyObject的列表,其中每个对象的date字段的hour各占据一个小时
* @return 返回一个MyObject列表,且每个对象的hour从0-23分布
*/
public static List<MyObject> doThings() {
	List<MyObject> list = new ArrayList<>();
	Date date = new Date();
	for (int i = 0; i < 24; i++) {
		date.setHours(i);
		list.add(new MyObject("name", date));
	}
	return list;
} 

运行这两段代码,能得到我们期望的结果吗?看一下下面的分析:

每个MyObject的对象都拥有一个Date对象,在使用其构造器前都新setdatehour值,应该得到期望的结果才对呀?并不,因为每个MyObject对象拥有的date的索引是完全一样的!也就是说最后得到的结果是:大家的hour全都是23,每次调用setHours(),都还“顺便”修改了以前创建好的MyObject对象的date字段。

你可能会想:在spec中明确要求客户端不要使用set()方法,而改成构造新的Date对象。但你永远不知道你会遇到怎样的client……为了安全,我们不应该这么想,应该从我们的代码中寻求解决方法。

那么应该如何改进?通常我们能够采用以下几种方法:

  1. 防御式拷贝(defensive copying)
    将第一个案例代码中的return语句修改为:

    	return new ArrayList<>(edges);
    

    这就是防御式拷贝的基本思想,当返回对象是mutable的对象时,不要返回原始数据,而返回一个拷贝。
    对于第二个案例,可以考虑将MyObject的构造器作出如下修改:

    	this.date = new Date(date.getTime());
    

    这样作出一份拷贝,无论客户端是否构造了新的Date,我们都能得到期望结果。(实际上,由于Date是可变的这一原因,它早就被废弃了,实际开发中最好使用其他的类来取代Date。)

  • copyclone()
    通常一个可变的类需要提供一个拷贝构造器方法以复制字段内容,但是产生拥有新地址的新对象供客户端使用。clone()也是相似原理,但并不是所有类都支持clone()
  • 最好的方法
    这“最好的方法”可能听起来有些不负责,但它的确是最安全的,那就是:永远尽量避免把类设计成可变的!并且,规定好我们的RIRep Invariant,表示不变性),写好checkRep()方法,最好在每一个方法(包括constructor, producer, observer, mutator)之后调用此方法,以检查当前类是否依然遵循RI,尤其是对于motator,我们要确保它做出的每一个修改都是受控的,如果不遵循,要及时采取措施,避免错误加深。

此前,我从来没有考虑过这个问题。


评论