实现Cloneable接口,表明对象是可以被克隆的,它决定Object中受保护方法cloen实现的行为,如果实现了Cloneable,Object的clone方法返回该对象的逐域拷贝,否则抛出CloneNotSupportException异常。
这是接口的极端用法,不值得效仿
clone方法不会调用对象的构造方法
clone方法返回一个该对象的拷贝,这个拷贝的精确含义取决于该对象的类,一般含义是:
对于任意的对象x,表示式:
x.clone() != x;
将会是true,并且表达式
x.clone().getCass() == x.getClass()
将会是ture,但是这都不是绝对的,虽然通常情况下,表达式
x.clone().equals(x)
将会是true,但是也不是绝对的
对于clone方法:如果你覆盖了非final类中的clone方法,则应该返回一个通过调用super.clone而得到的对象,如果所有超类都这么做,那么调用super.clone方法最终会调用Object.clone方法,从而创建出正确的实例,这种机制大体上类似于自动的构造器调用链,只不过他不是强制要求的。
从super.clone()中得到的对象可能接近于最重要返回的对象,也可能相差甚远,这取决于这个类的本质。
如果对象的每个域包含一个基本类型的值,或者包含一个指向不可变对象的引用,那么返回的对象则可能正式你所需要的对象,这种情况不需要再做处理,如前面章节的示例PhoneNumber:
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
throw new AssertionError();//不会发生
}
}
clone返回的类型是PhoneNumber,而不是Object,从Java1.5开始这么做是合法的。
如果对象中包含的域引用了可变对象,使用上面简单的clone方法可能造成灾难性的结果,如下Stack类:
public static class Stack implements Cloneable {
private Object[] objects;
private int size;
public static final int DEFAULT_SIZE = 16;
public Stack() {
objects = new Object[DEFAULT_SIZE];
}
public void push(Object o) {
ensureCapacity();
objects[size++] = o;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object o = objects[--size];
objects[size] = null;
return o;
}
private void ensureCapacity() {
if (size == objects.length) {
objects = Arrays.copyOf(objects, 2 * size + 1);
}
}
//just for test
public Object get() {
return objects;
}
@Override
public Stack clone() {
try {
return (Stack) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
throw new AssertionError();
}
}
}
我们在客户端调用:
Stack stack = new Stack();
stack.push(12);
Stack stack1 = stack.clone();
System.out.println(stack.get() == stack1.get());
结果是true
这表明被clone的Stack1对于所引用的objects与原Stack所引用的objects是相同的数据实例,修改原始实例会破坏被克隆对象中的约束条件,反之亦然。
实际上clone方法就是另一个构造器,必须确保它不会伤害到原始的对象,并确保正确的创建被克隆,为了使Stack能够正常工作,它必须要拷贝栈的内部信息。如:
@Override
public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.objects = objects.clone();
return result;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
throw new AssertionError();
}
}
同时我们还需要注意,如果elements域是final的,上面方案就不能正常工作,因为clone方法是禁止给final域赋新值的,clone框架与引用可比变对象的fianl域的正常用法是不相兼容的,除非原始对象可与被克隆对象之间安全的共享此可变对象。为了使类成为可以克隆的,可能有必要从某些域中去掉final修饰符。
但是对付更加复杂的对象上面方式实现的clone方法可能还不够,比如HashTable这样的类:
public static class HashTable implements Cloneable{
private Entry[] buckets = ...;
private static class Entry{
final Object key;
Object value;
Entry next;
public Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
}
}
虽然被克隆的对象有它自己的散列桶数组,但是这个数组引用的链表与原始的对象是一样的,从而很容易引起克隆对象和原始对象的不确定行为。为了修复这个问题,我们必须单独的拷贝并组成每个桶的链表。如:
public static class HashTable implements Cloneable {
private Entry[] buckets = ...;
@Override
protected HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = new Entry[buckets.length];
int length = buckets.length;
for (int i = 0; i < length; i++) {
if (buckets[i] != null) {
result.buckets[i] = buckets[i].deepCopy();
}
}
return result;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
private static class Entry {
final Object key;
Object value;
Entry next;
Entry deepCopy() {
return new Entry(key, value, next == null ? null : next.deepCopy());
}
public Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
}
}
或者为了防止递归造成的栈溢出,深度拷贝用如下实现:
Entry deepCopy2() {
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;
}
简而言之:所有实现了Cloneable接口的类都应该用一个共有的方法覆盖clone,此方法首先调用super.clone(),然后修正任何需要修正的域。这意味着要拷贝任何包含内部“深层结构”的可变对象。
更多细节可以参考《EffectiveJava》
如果一个对象太过于复杂,可以考虑采用其他方式实现对象拷贝,如提供一个拷贝构造器,它唯一的参数就是包含该构造器的类。如:
public A(A a){
A copyA = new A();
...
return coayA;
}
拷贝构造器与静态工厂方法的变形,都比Cloneable/clone方具有更多的优势,它们不依赖于很有风险的语言之外的对象创建机制,不需要遵守某些规范….。
更进一步,拷贝构造器或者拷贝工厂可以带有一个参数,参数类型是通过该类实现的接口,由此可以实现转换构造器:
public HashMap newHashMap(Map map){
....
}
public TreeSet newTreeSet(Set set){
...
}
既然Cloneable有这么多问题,可以肯定的说,其他接口都不应该扩展这个接口,为了继承而设计的类也不应该实现这个接口,甚至有很多专家程序员从来不覆盖clone方法,也从来不用,除非是数组拷贝。