谨慎地重写 clone 方法
Cloneable 接口的目的是作为一个 mixin 接口 ,公布这样的类允许克隆。
不幸的是,它没有达到这个目的。它的主要缺点是缺少 clone 方法,而 Object 的 clone 方法是受保护的。
你不能,不借助反射,仅仅因为它实现了 Cloneable 接口,就调用对象上的 clone 方法。
即使是反射调用也可能失败,因为不能保证对象具有可访问的 clone 方法。
Cloneable 接口决定了 Object 的受保护的 clone 方法实现的行为:如果一个类实现了 Cloneable 接口,那么 Object 的 clone 方法将返回该对象的逐个属性(field-by-field)拷贝;否则会抛出 CloneNotSupportedException
异常。
这是一个非常反常的接口使用,而不应该被效仿。 通常情况下,实现一个接口用来表示可以为客户做什么。但对于 Cloneable 接口,它会修改父类上受保护方法的行为。
普通克隆
假设你希望在一个类中实现 Cloneable 接口,它的父类提供了一个行为良好的 clone 方法。首先调用 super.clone。 得到的对象将是原始的完全功能的复制品。 在你的类中声明的任何属性将具有与原始属性相同的值。 如果每个属性包含原始值或对不可变对象的引用,则返回的对象可能正是你所需要的,在这种情况下,不需要进一步的处理。
示例代码:Item13Example01.java:实现 Cloneable 接口的 clone 方法。
对象包含引用可变对象的属性
如果对象包含引用可变对象的属性,则前面显示的简单 clone 实现可能是灾难性的。
- Item13Example02.java: 如果 clone 方法仅返回 super.clone() 调用的对象,那么生成的 Stack 实例在其 size 属性中具有正确的值,但 elements 属性引用与原始 Stack 实例相同的数组。
- Item13Example03.java:为了使 Stack 上的 clone 方法正常工作,它必须复制 stack 对象的内部。
克隆 final 属性
如果属性是 final 的,则以前的解决方案将不起作用,因为克隆将被禁止向该属性分配新的值。
这是一个基本的问题:像序列化一样,Cloneable 体系结构与引用可变对象的 final 属性的正常使用不兼容,除非可变对象可以在对象和其克隆之间安全地共享。
为了使一个类可以克隆,可能需要从一些属性中移除 final 修饰符。
-
Item13Example04.java: 只是递归地克隆哈希桶数组。
虽然被克隆的对象有自己的哈希桶数组,但是这个数组引用与原始数组相同的链表,这很容易导致克隆对象和原始对象中的不确定性行为。
-
Item13Example05.java:复制包含每个桶的链表。
如果哈希桶不是太长,这种技术很聪明并且工作正常。
但是,克隆链表不是一个好方法,因为它为列表中的每个元素消耗一个栈帧(stack frame)。
如果列表很长,这很容易导致堆栈溢出。
-
Item13Example06.java:用迭代来替换 deepCopy 中的递归。
复制构造方法或复制工厂
对象复制更好的方法是提供一个复制构造方法或复制工厂。
复制构造方法接受参数,其类型为包含此构造方法的类,例如:
// 复制构造方法
public Yum(Yum yum) { ... };
// 复制工厂
public static Yum newInstance(Yum yum) { ... };
复制构造方法及其静态工厂变体与 Cloneable/clone 相比有许多优点:
- 它们不依赖风险很大的语言外的对象创建机制;
- 不要求遵守那些不太明确的惯例;
- 不会与 final 属性的正确使用相冲突;
- 不会抛出不必要的检查异常;
- 不需要类型转换。
此外,复制构造方法或复制工厂可以接受类型为该类实现的接口的参数。 例如,按照惯例,所有通用集合实现都提供了一个构造方法,其参数的类型为 Collection 或 Map。 基于接口的复制构造方法和复制工厂(更适当地称为转换构造方法和转换工厂)允许客户端选择复制的实现类型,而不是强制客户端接受原始实现类型。 例如,假设你有一个 HashSet,并且你想把它复制为一个 TreeSet。 clone 方法不能提供这种功能,但使用转换构造方法很容易:new TreeSet<>(s)
。
考虑到与 Cloneable 接口相关的所有问题,新的接口不应该继承它,新的可扩展类不应该实现它。 虽然实现 Cloneable 接口对于 final 类没有什么危害,但应该将其视为性能优化的角度,仅在极少数情况下才是合理的。
通常,复制功能最好由构造方法或工厂提供。 这个规则的一个明显的例外是数组,它最好用 clone 方法复制。