【第24条】亟需时使用保护性拷贝
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
http://en.wikipedia.org/wiki/Copy-on-write
如果不是immutable,可能经常要进行保护性拷贝
http://tonylian.iteye.com/blog/391256,保护性拷贝
保护性拷贝直接double了内存的占用
而且,如果在循环中进行保护性拷贝,那就不得了了
scala在后面折腾这些都是以性能为代价的。当然JVM本身也折腾,scala是进一步折腾 :-)