Java受欢迎的一个重要原因是它是一门安全的语言。它对于缓冲区溢出、数组越界、非法指针以及其他内存破坏错误自动免疫。
但是,这并不是说你可以高枕无忧,正如前面【第5条】中所述的,某些情况下你还是要自行回收过期引用的。现在我们再来说一下你不得不做的“自我防卫”性工作。
【第5条】中的回收过期引用,即使你没有这么做,顶多是浪费一些内存资源。但是,如果本条所述的“自我防卫”工作你没有到位的话,那后果就可能是灾难性的了,而其错误所在往往不容易被发现。
如果一个方法或构造函数允许可变对象进/出,那么就要考虑一下使用者是否有可能改变它。如果是的话,那你必须对该对象进行保护性拷贝,使进入方法内部的对象是外部时的拷贝而不它本身(因为外部的对象有可能还会被改变)。
public class Stuff {
private String name;
private Date birthday;
public Stuff(String name, Date birthday) {
this.name = name;
this.birthday = birthday;
}
public Date getBrithday() {
return this.birthday;
}
}
这样的一个职员类,它包括姓名和生日。在没有进行保护性拷贝的保护之前,我们来看看攻击者(攻击者太极端了,很肯能是一个“高级”程序员)是如何开始他的破坏工作的:
Date day1 = new Date(1970,1,1)
Stuff zhangSan = new Stuff("张三", day1);
// .......
// 几行其他代码之后,老先生有想起 day1 来了
day1.setYear(2009);
// 这下坏了,张三先生成了婴儿了,zhangSan.birthday = 2009-1-1
这样的原因就是Date型是一个可变类型(Java早期遗留下来的遗憾之一,详见【第13条】)。我们使用保护性拷贝来防之:
public Stuff(String name, Date birthday) {
this.name = name;
this.birthday = new Date(birthday.getTime()); // 内部的birthday实际上在这里新创建的一个“日期值”等于实参的新实例
// 因为,Date型是可变类型,所以“值”虽相等,也是新实例
}
现在这位老先生无论怎么折腾 day1 也不会影响到 张三 大哥的生辰八字了。但是,人家老先生还有高招:
zhangSan.getBirthday().setYear(2010);
// 这回张三先生就更惨了,直接回娘胎了
如果getBrithday方法是我们必须提供的,那该怎么办呢?答案还是保护性拷贝:
public Date getBrithday() {
return (Date) this.birthday.clone(); // 我造个新的实例扔给你,你随便折腾吧,影响不到我
}
看到了吧,这就是保护性拷贝,使用构造函数也好,克隆也罢,总之是得到一个原对象的副本。
最后要提一下包装类模式(包括适配器模式和装饰模式),根据包装类的本质特征,使用者只需直接访问被包装的对象,就可以破坏包装类的约束条件,但是,这样做的前提是确保不会伤害使用者自己。
【Effective Java 学习笔记】系列连载专题请见:
http://tonylian.iteye.com/categories/64208