Files
effective-Java/ch03所有对象的通用方法/13.谨慎地重写clone方法.md
2019-10-10 20:10:34 +08:00

82 lines
5.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 谨慎地重写 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](MethodsCommonToAllObjects/src/main/java/com/jueee/item13/Item13Example01.java):实现 Cloneable 接口的 clone 方法。
### 对象包含引用可变对象的属性
如果对象包含引用可变对象的属性,则前面显示的简单 clone 实现可能是灾难性的。
- [Item13Example02.java](MethodsCommonToAllObjects/src/main/java/com/jueee/item13/Item13Example02.java)
如果 clone 方法仅返回 super.clone() 调用的对象,那么生成的 Stack 实例在其 size 属性中具有正确的值,但 elements 属性引用与原始 Stack 实例相同的数组。
- [Item13Example03.java](MethodsCommonToAllObjects/src/main/java/com/jueee/item13/Item13Example03.java):为了使 Stack 上的 clone 方法正常工作,它必须复制 stack 对象的内部。
### 克隆 final 属性
如果属性是 final 的,则以前的解决方案将不起作用,因为克隆将被禁止向该属性分配新的值。
这是一个基本的问题像序列化一样Cloneable 体系结构与引用可变对象的 final 属性的正常使用不兼容,除非可变对象可以在对象和其克隆之间安全地共享。
为了使一个类可以克隆,可能需要从一些属性中移除 final 修饰符。
- [Item13Example04.java](MethodsCommonToAllObjects/src/main/java/com/jueee/item13/Item13Example04.java)
只是递归地克隆哈希桶数组。
虽然被克隆的对象有自己的哈希桶数组,但是这个数组引用与原始数组相同的链表,这很容易导致克隆对象和原始对象中的不确定性行为。
- [Item13Example05.java](MethodsCommonToAllObjects/src/main/java/com/jueee/item13/Item13Example05.java):复制包含每个桶的链表。
如果哈希桶不是太长,这种技术很聪明并且工作正常。
但是克隆链表不是一个好方法因为它为列表中的每个元素消耗一个栈帧stack frame
如果列表很长,这很容易导致堆栈溢出。
- [Item13Example06.java](MethodsCommonToAllObjects/src/main/java/com/jueee/item13/Item13Example06.java):用迭代来替换 deepCopy 中的递归。
### 复制构造方法或复制工厂
对象复制更好的方法是提供一个复制构造方法或复制工厂。
复制构造方法接受参数,其类型为包含此构造方法的类,例如:
```java
// 复制构造方法
public Yum(Yum yum) { ... };
```
```java
// 复制工厂
public static Yum newInstance(Yum yum) { ... };
```
复制构造方法及其静态工厂变体与 Cloneable/clone 相比有许多优点:
- 它们不依赖风险很大的语言外的对象创建机制;
- 不要求遵守那些不太明确的惯例;
- 不会与 final 属性的正确使用相冲突;
- 不会抛出不必要的检查异常;
- 不需要类型转换。
此外,复制构造方法或复制工厂可以接受类型为该类实现的接口的参数。 例如,按照惯例,所有通用集合实现都提供了一个构造方法,其参数的类型为 Collection 或 Map。 基于接口的复制构造方法和复制工厂(更适当地称为转换构造方法和转换工厂)允许客户端选择复制的实现类型,而不是强制客户端接受原始实现类型。 例如,假设你有一个 HashSet并且你想把它复制为一个 TreeSet。 clone 方法不能提供这种功能,但使用转换构造方法很容易:`new TreeSet<>(s)`
考虑到与 Cloneable 接口相关的所有问题,新的接口不应该继承它,新的可扩展类不应该实现它。 虽然实现 Cloneable 接口对于 final 类没有什么危害,但应该将其视为性能优化的角度,仅在极少数情况下才是合理的。
通常,复制功能最好由构造方法或工厂提供。 这个规则的一个明显的例外是数组,它最好用 clone 方法复制。