先看一段代码,这是我最初在设计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
对象,在使用其构造器前都新set
了date
的hour
值,应该得到期望的结果才对呀?并不,因为每个MyObject
对象拥有的date
的索引是完全一样的!也就是说最后得到的结果是:大家的hour
全都是23,每次调用setHours()
,都还“顺便”修改了以前创建好的MyObject
对象的date
字段。
你可能会想:在spec
中明确要求客户端不要使用set()
方法,而改成构造新的Date
对象。但你永远不知道你会遇到怎样的client……为了安全,我们不应该这么想,应该从我们的代码中寻求解决方法。
那么应该如何改进?通常我们能够采用以下几种方法:
-
防御式拷贝(defensive copying)
将第一个案例代码中的return语句修改为:return new ArrayList<>(edges);
这就是防御式拷贝的基本思想,当返回对象是mutable的对象时,不要返回原始数据,而返回一个拷贝。
对于第二个案例,可以考虑将MyObject
的构造器作出如下修改:this.date = new Date(date.getTime());
这样作出一份拷贝,无论客户端是否构造了新的
Date
,我们都能得到期望结果。(实际上,由于Date
是可变的这一原因,它早就被废弃了,实际开发中最好使用其他的类来取代Date
。)
copy
和clone()
通常一个可变的类需要提供一个拷贝构造器方法以复制字段内容,但是产生拥有新地址的新对象供客户端使用。clone()
也是相似原理,但并不是所有类都支持clone()
。- 最好的方法
这“最好的方法”可能听起来有些不负责,但它的确是最安全的,那就是:永远尽量避免把类设计成可变的!并且,规定好我们的RI
(Rep Invariant
,表示不变性),写好checkRep()
方法,最好在每一个方法(包括constructor
,producer
,observer
,mutator
)之后调用此方法,以检查当前类是否依然遵循RI
,尤其是对于motator
,我们要确保它做出的每一个修改都是受控的,如果不遵循,要及时采取措施,避免错误加深。
此前,我从来没有考虑过这个问题。
评论区