《Effective Java》读书笔记09--谨慎地覆盖clone方法
一、Cloneable接口与Object.clone方法
Cloneable接口的目的是作为对象的一个mixin接口(混合型接口),表明这样的对象允许克隆(clone)。遗憾的是Cloneable接口里并没有clone方法,其实它什么方法都没有,跟Serializable接口一样,都是占着茅坑不拉屎。它只是声明该对象可以被克隆,具体行为由类设计者决定。如果类设计者忘记提供一个良好的clone方法或根本不提供clone方法,那么类客户使用时必定会出错,这样Cloneable接口并没达到它的目的。
Cloneable接口决定了Object中受保护的clone方法的行为:如果一个类实现了Cloneable,Object的clone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException异常。这是接口的极端非典型的用法,不值得效仿,因为它违背了接口的使用规范,改变了超类中受保护的方法的行为。
Cloneable接口约束实现类及其所有超类都必须遵守一种语言之外的机制:无需调用构造器就可以创建对象。通常这种机制的通用约定是非常弱的。它要求实现类和父类必须提供一个Clone方法,在Clone方法中调用super.clone方法,已达到最终调用Object.clone方法来完成约定。如果有一层没有按照约定实现,那么该类的Clone功能将是潜在的灾难。
二、Object中Clone方法的通用约定
Clone方法用于创建和返回对象的一个拷贝,一般含义如下:
1、对于任何对象x,表达式 x.clone()!=x 将会是true,并且表达式 x.clone().getClass() == x.getClass()将会是true,但这不是绝对要求。
2、通常情况下,表达式 x.clone.equals(x)将会是true,同1一样这不是绝对要求。
拷贝对象往往会导致创建它的类的一个新实例,但它同时也要求拷贝内部的数据接口,这个过程中没有调用构造器。
该通用约定存在的问题:
1、不调用构造器的规定太强硬
行为良好的clone方法可以调用构造器来创建对象,构造之后再复制内部数据。如果这个类是final的,clone甚至可能会返回一个由构造器创建的对象。既然类是final的,不可变的,我当然可以调用构造器创建一个实例,甚至缓存起来(单例模式),等调用clone时直接返回该对象,这样效率更高。
2、x.clone().getClass()通常应该等同于x.getClass()的规定太软弱
在实践中,我们一般会假设:如果扩展一个类,并在子类中调用了super.clone,返回的对象就将是该子类的实例(我们要克隆的是子类而不是父类)。
超类提供此功能的唯一途径是:返回一个通过调用super.clone而得到的对象。如果clone方法返回一个由构造器创建的对象,它就会得到错误的类(当前父类而不是想要的子类)。
因此,如果你覆盖了非final类中的clone方法,则应该返回一个通过调用super.clone而得到的对象。如果类的所有超类都遵守这条规则,那调用super.clone方法最终会调用Object.clone方法,从而创建正确类的实例,此机制类似于自动的构造器调用链,只不过它不是强制要求的。
三、实现一个行为良好的clone方法
从super.clone()中得到的对象有时接近于最终要返回的对象,有时会相差很远,这取决于该类的本质。
1、每个域包含的只有基本类型或指向不可变对象的引用,这种情况返回的对象可能满足我们的需要,比如《读书笔记08》中的PhoneNumber类。在此,我们只需声明实现Cloneable接口,然后对Object中受保护的clone方法提供公有的访问途径:
@Override public PhoneNumber clone() { try { return (PhoneNumber) super.clone();//协变返回类型,永远不要让客户去做任何类库能够替他完成的事情。 } catch(CloneNotSupportedException e) { throw new AssertionError(); // Can't happen } }
2、域中包含可变对象,如《读书笔记05》中的Stack类。
如果想把该类做成cloneable的,如果它的clone方法仅仅返回super.clone(),这样得到的Stack实例虽然size域具有正确的值(基本类型),但它的elements域将引用与原始Stack实例相同的数组。修改原始的实例会破坏被克隆对象中的约束条件,反之亦然。
clone方法就是另一个构造器,你必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件。
修改版:
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();//递归调用clone。如果elements是final的,则需要把final去掉,因为final使得elements域不能被赋新值。另外,在数组上调用clone返回是数组,并且它的编译时类型与被克隆数组的类型相同
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
3、变量中的变量之深度拷贝
有时候递归地调用 clone还不够。比如,自己实现一个散列表并为它编写clone方法,它的内部数据包含一个散列桶数组,每个散列桶都指向"键-值"对链表的第一个项,如果桶是空的,则为null。出于性能方面的考虑,该类实现了自己的轻量级单向链表,而没有使用java内部的java.util.LinkedList,具体类实现如下:
public class HashTable implements Cloneable{ private Entry[] buckets = ...; private static class Entry{ final Object key; Object value; Entry next; Entry(Object key,Object value,Entry next){ this.key = key; this.value = value; this.next = next; } ....//Remainder omitted }
假如我们仅仅像对Stack那样递归地克隆这个散列桶数组,如下:
//Broken - results in shared internal state! @Override public HashTable clone(){ try{ HashTable result = (HashTable) super.clone(); result.buckets = buckets.clone(); return result; }catch(CloneNotSupportedException e){ throw new AssertionError(); } }
虽然被克隆的对象有它自己的散列桶数组,但这个数组引用的链表与原始对象是一样的,从而容易引起克隆对象和原始对象中不确定的行为。为修正该问题,需要单独地拷贝并组成每个桶的链表,下面是一种常用做法:
public class HashTable implements Cloneable{ private Entry[] buckets = ...; private static class Entry{ final Object key; Object value; Entry next; Entry(Object key,Object value,Entry next){ this.key = key; this.value = value; this.next = next; } //Recursively copy the linked list headed by this Entry Entry deepCopy(){ return new Entry(key,value,next==null?null:next.deepCopy()); } } //DeepCopy @Override public HashTable clone(){ try{ HashTable result = (HashTable) super.clone(); result.buckets = new Entry[buckets.length]; for(int i=0; i<buckets.length; i++){ if(buckets[i]!=null) result.buckets[i] = buckets[i].deepCopy(); } return result; }catch(CloneNotSupportedException e){ throw new AssertionError(); } } }
私有类HashTable.Entry被加强了,支持深度拷贝。此方法虽然很灵活,但如果链表比较长,则很容易导致栈溢出,列表中的每个元素都要消耗一段栈空间的。可以采用迭代来代替递归,如下:
//Iteratively copy the linked list headed by this Entry Entry deepCopy(){ Entry result = new Entry(key,value,next); for(Entry p = result; p.next!=null; p=p.next) p.next = new Entry(p.next.key,p.next.value,p.next.next); return result; }
克隆复杂对象的最后一种办法:先调用super.clone,然后把结果对象中的所有域都设置成它的空白状态,然后调用高层(higher-level)的方法来重新产生对象的状态。这种方式简单,合理且优美,但运行速度通常没有"直接操作对象及其克隆对象的内部状态的clone方法"快。
四、clone方法的替代品
如果我们扩展一个实现了Cloneable接口的类,那么除了实现一个行为良好的clone方法外,没有别的选择。否则,最好提供某些其他的途径来代替对象拷贝,或者干脆不提供这样的功能。
实现对象拷贝的好方法:
提供一个拷贝构造器或拷贝工厂
//Copy constructor public Yum(Yum yum); //Copy factory public static Yum newInstance(Yum yum);
此方式优点:
1、它们不依赖于某一种很有风险的,语言之外的对象创建机制。
2、它们不要求遵守尚未制定好的文档规范。
3、它们不会与final的正常使用发生冲突。
4、它们不会抛出不必要的受检异常。
5、它们不需要进行类型转换。
6、它们可以带一个参数,参数类型是通过该类实现的接口,比如集合框架。
五、最佳编程实践
如果必须提供clone方法:
1、clone方法不应该在构造的过程中,调用新对象中任何非final的方法,会造成克隆对象与原始对象的状态不一致。
2、公有的clone方法应该省略CloneNotSupportException异常,因为这样使用起来更轻松。如果专门为了继承而设计的类覆盖了clone方法,覆盖版本的clone方法就应该模拟Object.clone的行为:它应该被声明为protected,抛出CloneNotSupportException异常,并且该类不应该实现Cloneable接口,以便子类可以自己决定是否实现它。
3、用线程安全的类实现Cloneable接口,要记得它的clone方法必须得到很好地同步。
4、任何实现了Cloneable接口的类都应该用一个公有的方法覆盖clone。此方法首先调用super.clone,然后修正任何需要修正的域。
5、使用拷贝构造器或拷贝工厂来代替clone方法