【Effective Java 13】谨慎地覆盖 clone
1. Object 中对 clone 接口的规范
clone
方法的通用约定是非常弱的,下面是来自 Object
规范中的约定内容:
创建和返回该对象的一个拷贝。这个 “拷贝” 的精确含义取决于该对象的类。一般的含义是,对于任何对象 x
,有:
x.clone() != x; // true
x.clone.getClass() == x.getClass(); // true
x.clone().equals(x); // true
但这些要求不是绝对的。
2. clone 接口的规范
-
永远不要给不可变的类永远都不应该提供
clone
方法。 -
clone 方法就是另一个构造器,必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约定条件。比如当类中有数组时,要注意拷贝数组,而不是拷贝引用。
public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone(); // 拷贝数组, 但 elements 必须是可变的
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
- 就像序列化一样,Cloneable 架构与引用可变对象的 final 域的正常用法是不兼容的,除非在原始对象和克隆对象之间以安全地共享此可变对象。为了使类成为可克隆的,可能有必要从某些域中去掉 final 修饰符。
3. 更好的 clone 方法
对象拷贝的更好的方法是提供一个拷贝构造器(copy constructor)或拷贝工厂(copy factory)。拷贝构造器只是一个构造器,它唯一的参数类型是包含该构造器的类,而拷贝工厂是类似于拷贝构造器的静态工厂:
// 拷贝构造器
public Yum(Yum yum) { ... }
// 拷贝工厂方法
public static Yum newInstance(Yum yum) { ... }
拷贝构造器的做法,及其静态工厂方法的变形,都比 Cloneable/clone 方法具有更多优势:它们不依赖于某一种很有风险的、语言之外的对象创建机制;它们不要求遵守尚未制定好的文档规范;它们不会与 final 域的正常使用发生冲突;它们不会抛出不必要的受检异常;它们不需要进行类型转换。
甚至,拷贝构造器或者拷贝工厂可以带一个参数,参数类型是该类实现的接口。例如,按照惯例所有通用集合实现都提供了一个拷贝构造器,其参数类型为 Collection 或者 Map 接口。基于接口的拷贝构造器和拷贝工厂(更准确的叫法应该是转换构造器、转换工厂),允许客户选择拷贝的实现类型,而不是强迫客户接受原始的实现类型。例如,假设你有一个 HashSet:s,并希望把他拷贝成一个 TreeSet。clone 方法无法提供这样的功能,但是转换构造器很容易实现:new TreeSet<>(s)
。
4. 总结
复制最好由构造器或者工厂提供,数组除外。对于数组对象,还是使用 clone
方法,或 Arrays.copyOf()