第11条:谨慎地覆盖clone
Cloneable
接口的目的是作为对象的的一个mixin接口(mixin interface)(见第18条),表明这样的对象允许克隆(clone)。遗憾的是,它并没有成功地达到这个目的。其主要的缺陷在于,它缺少一个clone
方法,Object
的clone
方法是受保护的。如果不借助于反射(reflection)(见第53条),就不能仅仅因为一个对象实现了Cloneable
,就可以调用clone
方法。即使是反射调用也可能会是该,因为不能保证该对象一定具有可访问的clone
方法。尽管存在这样那样的缺陷,这项设施仍然被广泛地使用着,因此值得我们进一步地了解。本条目将告诉你如何实现一个行为良好的clone
方法,并讨论何时适合这样做,同时也简单地讨论了其他的可替换做法。
既然Cloneable
并没有包含任何方法,那么它到底有什么作用呢?它决定了Object
中受保护的clone
方法实现的行为:如果一个类实现了Cloneable
,Object
的clone
方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException
异常。这是接口的一种极端非典型的用法,也不值得效仿。通常情况下,实现接口是为了表明类可以为它的客户做些什么。然而,对于Cloneable
接口,它改变了超类中受保护的方法的行为。
如果实现Cloneable
接口是要对某个类起到租用,类和它的所有超类都必须遵守一个相当复杂的、不可实施的,并且基本上没有文档说明的协议。由此得到一种语言之外的(extralinguistic)机制:无需调用构造器就可以创建对象。
Clone
方法的通用约定是非常弱的,下面是来自java.lang.Object
规范中的约定内容[JavaSE6]:
创建和返回对象的一个拷贝。这个“拷贝”的精确含义取决于该对象的类。一般的含义是,对于任何对象x
,表达式
x.clone() != x
将会是true
,并且,表达式
x.clone().getClass() == x.getClass()
将会是true
,但这些都不是绝对的要求。虽然通常情况下,表达式
x.clone().equals(x)
将会是true
,但是,这也不是一个绝对的要求。拷贝对象的往往会导致创建它的类的一个新实例,但它同时也会要求拷贝内部的数据结构。这个过程中没有调用构造器。
这个约定存在几个问题。“不调用构造器”的规定太强硬了。行为良好的clone
方法可以调用构造器来创建对象,构造之后再复制内部数据。如果这个类是final
的,clone
甚至可能会返回一个由构造器创建的对象。
然而,x.clone().getClass()
通常应该等同于x.getClass()
的规定又太软弱了。在实践中,程序员会假设:如果他们扩展了一个类,并且从子类中调用了super.clone
,返回的对象就将是该子类的实例。超类能够提供这种功能的唯一途径是,返回一个通过调用super.clone
而得到的对象。如果clone
方法返回一个由构造器创建的对象,它就得到有错误的类。因此,如果你覆盖了非final
类中的clone
方法,则应该返回一个通过调用super.clone
而得到的对象。如果类的所有超类都遵守这条规则,那么调用super.clone
最终会调用Object
的clone
方法,从而创建出正确类的实例。这种机制大体上类似于自动的构造器调用链,只不过它不是强制要求的。
从1.6发行版本开始,Cloneable
接口并没有清楚地指明,一个类在实现这个接口时应该承担哪些责任。实际上,对于实现了Cloneable
的类,我们总是期望它提供一个功能适当的公有的clone
方法。通常情况下,除非该类的所有超类都提供了行为良好的clone
实现,无论是公有的还是受保护的,否则,都不可能这么做。
假设你希望在一个类中实现Cloneable
,并且它的超类都提供行为良好的Clone
方法。你从super.clone()
中得到的对象可能会接近于最终要返回的对象,也可能相差甚远,这要取决于这个类的本质。从每个超类的角度来看,这个对象僵尸原始对象功能完整的克隆(clone)。在这个类中声明的域(如果有的话)将等同于被克隆对象中相应的域。如果每个域包含一个基本类型的值,或者包含一个指向不可变对象的引用,那么被返回对象则可能正是你所需要的对象,在这种情况下不需要再做进一步处理。例如,第9条中的PhoneNumber
类正是如此。在这种情况下,你所需要做的,除了声明实现了Cloneable
之外,就是对Object
中受保护的clone
方法提供公有的访问途径:
@Override public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // Can't happen
}
}
注意上述的clone
方法返回的是PhoneNumber
,而不是Object
。从Java 1.5发行版本开始,这么做是合法的,也是我们所期待的,因为1.5发行版本中引入了协变返回类型(covariant return type)作为泛型。换句话说,目前覆盖方法的返回类型可以是被覆盖方法的返回类型的子类了。这样有助于覆盖方法提供更多关于被返回对象的信息,并且在客户端中不必进行转换。由于Object.clone
返回Object
,PhoneNumber.clone
必须在返回super.clone()
的结果之前将它转换。这里提现了一条通则:永远不要让客户去做任何类库能够替客户完成的事情。
如果对象中包含的域引用了可变的对象,使用上述这种简单的clone
实现可能会导致灾难性的后果。例如,考虑第6条中的Stack
类:
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
// Ensure space for at least one more element.
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
假设你希望把这个类做成可克隆的(cloneable)。如果它的clone
方法仅仅返回super.clone()
,这样得到的Stack
实例,在其size
域中具有正确的值,但是它的elements
域将引用与原始Stack
实例相同的数组。修改原始的实例会破坏被克隆对象中的约束条件,反之亦然。很快你就会发现,这个程序将产生毫无意义的结果,或者抛出NullPointerException
异常。
如果调用Stack
类中唯一的构造器,这种情况就永远不会发生。实际上,clone
方法就是另一个构造器;你必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件(invariaant)。为了使Stack
类中的clone
方法正常地工作,它必须要拷贝栈的内部信息。最容易的做法是,在elements数组中递归的调用clone
:
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
注意,我们不一定要将elements.clone()
的结果转换成Object[]
。自Java 1.5发行版本起,在数组上调用clone
返回的数组,其编译时类型与被克隆数组的类型相同。
还要注意,如果elements
域是final
的,上述方案就不能正常工作,因为clone
方法是被禁止给elements
域赋新值的。这是个根本的问题:clone
架构与引用可变对象的final
域的正常用法是不相兼容的,除非在原始对象和克隆对象之间可以安全地共享此可变对象。为了使类成为可克隆的,可能有必要从某些域中去掉final
修饰符。
递归地调用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());
}
}
@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();
}
}
... // Remainder omitted
}
私有类HashTable.Entry
被加强了,它支持一个“深度拷贝(deep copy)”方法。HashTable
上的clone
方法分配了一个大小适中的、新的buckets
数组,并且遍历原始的buckets
数组,对每一个非空散列桶进行深度拷贝。Entry
类中的深度拷贝方法递归地调用它自身,以便拷贝整个链表(它是链表的头结点)。虽然这种方法很灵活,如果散列桶不是很长的话,也会工作得很好,但是,这样克隆一个链表并不是一个好方法,因为针对列表中的每个元素,它都要消耗一端栈空间。如果链表比较长,这很容易导致栈溢出。为了避免发生这种情况,你可以在deepCopy
中用迭代(iteration)代替递归(recursion):
// 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
,然后把结果对象中的所有域都设置为它们的空白状态(virgin state),然后调用高层(higher-level)的方法来重新产生对象的状态。在我们的HashTable
例子中,buckets
域将被初始化为一个新的散列桶数组,然后,对于正在被克隆的散列表中的每一个键——值映射,都调用put(key, value)
方法(上面没有给出其代码)。这种做法往往会产生一个简单、合理且相当优美的clone
方法,但是它运行起来通常没有“直接操作对象及其克隆对象的内部状态的clone
方法”快。
如同构造器一样,clone
方法不应该在构造的过程中,调用新对象中任何非final
的方法(见第17条)。如果clone
调用了一个被覆盖的方法,那么在该方法所在的子类有机会修正它在克隆对象中的状态之前,该方法就会先被执行,这样很有可能会导致克隆对象和原始对象之间的不一致。因此,上一段落中讨论到的put(key, value)
方法应该要么是final
的,要么是私有的(如果是私有的,它应该算是非final
公有方法的“辅助方法[helper method]”)。
Object
的clone
方法被声明为可抛出CloneNotSupportedException
异常,但是,覆盖版本的clone
方法可能会忽略这个声明。公有的clone
方法应该省略这个声明,因为不会抛出受检异常(checked exception)的方法与会抛出异常的方法想必,使用起来更加轻松(见第59条)。如果专门为了继承而设计的类[见第17条]覆盖了clone
方法,覆盖版本的clone
方法就应该模拟Object.clone
的行为:它应该被声明为protected
、抛出CloneNotSupportedException
异常,并且该类不应该实现Cloneable
接口。这样做可以使子类具有实现或者不实现Cloneable
接口的自由,就仿佛它们直接扩展了Object
一样。
还有一点值得注意。如果你决定用线程安全的类实现Cloneable
接口,要记得它的clone
方法必须得到很好的同步,就像任何其他方法一样(见第66条)。Object
的clone
方法没有同步,因此即使很满意,可能也必须编写同步的clone
方法来调用super.clone
。
简而言之,所有实现了Cloneable
接口的类都应该用一个公有的方法覆盖clone
。此公有方法首先调用super.clone
,然后修正任何需要修正的域。一般情况下,这意味着要拷贝任何包含内部“深层结构”的可变对象,并用指向新对象的引用代替原来指向这些对象的引用。虽然,这些内部拷贝操作往往可以通过递归地调用clone
来完成,但这通常并不是最佳方法。如果该类只包含基本类型的域,或者指向不可变对象的引用,那么多半的情况是没有域需要修正。这条规则也有例外,譬如,代表序号或者其他唯一ID值的域,或者代表对象的创建时间的域,不管这些域是基本类型还是不可变的,它们也都需要被修正。
真的有必要这么复杂吗?很少有这种必要。如果你扩展一个实现Cloneable
接口的类,那么你除了实现一个行为良好的clone
方法外,没有别的选择。否则,最好提供某些其他的途径来代替对象拷贝,或者干脆不提供这样的功能。例如,对于不可变类,支持对象拷贝并没有太大的意义,因为被拷贝的对象与原始对象没有实质的不同。
另一个实现对象拷贝的好办法是提供一个拷贝构造器(copy constructor)或拷贝工厂(copy factory)。拷贝构造器只是一个构造器,它唯一的参数类型是包含该构造器的类,例如:
public Yum(Yum yum);
拷贝工厂是类似于拷贝构造器的静态工厂:
public static Yum newInstance(Yum yum);
拷贝构造器的做法,及其静态工厂方法的变性,都比Cloneable/clone
方法具有更多的优势:它们不依赖于某一种很有风险的、语言之外的对象创建机制;它们不要求遵守尚未制定好文档的规范;它们不会与final
域的正常使用发生冲突;它们不会抛出不必要的受检异常(checked exception);它们不需要进行类型转换。虽然你不可能把拷贝构造器或者静态工厂放到接口中,但是由于Cloneable
接口缺少一个公有的clone
方法,所以它也没有提供一个接口该有的功能。因此,使用拷贝构造器或者拷贝工厂来代替clone
方法时,并没有放弃接口的功能特性。
更进一步,拷贝构造器或者拷贝工程可以带一个参数,参数类型是通过该类实现的接口。例如,按照惯例,所有通用集合实现都提供了一个拷贝构造器,它的参数类型为Collection
或者Map
。基于接口的拷贝构造器和拷贝工厂(更准确的叫法应该是“转换构造器(conversion constructor)”和转换工厂(conversion fatory)),允许客户选择拷贝的实现类型,而不是强迫客户接受原始的实现类型。例如,假设你有一个HashSet
,并且希望把它拷贝成一个TreeSet
。clone
方法无法提供这样的功能,但是用转换构造器很容易实现:new TreeSet(s)
。
既然Cloneable
接口具有上诉那么多问题,可以肯定地说,其他的接口都不应该扩展(extend)这个接口,为了继承而设计的类(见第17条)也不应该实现(implement)这个接口。由于它具有这么多的缺点,有些专家级的程序员干脆从来不去覆盖clone
方法,也从来不去调用它,除非拷贝数组。你必须清楚一点,对于一个专门为了继承而设计的类,如果你未能提供行为良好的受保护的(protected)clone
方法,它的子类就不可能实现Cloneable
接口。