好艰深的一节,看了两三遍才完全吃透。
覆盖clone方法要谨慎
Java中一个广为人知的复制对象的方法是clone。这个方法看上去方便好用,但实际上问题和限制都很多,所以不推荐使用。m88体育 需要复制对象的情况下,应该尽可能使用复制构造函数或静态factory方法。
clone方法最大的问题来自于它的实现机制:Cloneable接口。这个接口严重破坏了Java语言中接口的使用规范,m88体育 所以被称为"extralinguistic"。通常情况下,一个接口需要声明一些方法,而实现接口的类必须覆盖这些方法以提供具体行为。这样的覆盖是强制性的,否则会产生编译错误。但Cloneable接口是一个特例,它没有声明任何方法,实际上是一个空接口,因而实现它的类不会被强制覆盖clone方法。此接口的唯一作用,是作为一个"开关",用来开放Object类中的clone方法。
默认情况下Object.clone()是可访问的,但会有CloneNotSupportedException弹出。举个例子:
class TestClone { /* clone() method of base class Object is not overriden. But as it's protected, it's still accessible */ public void test() throws CloneNotSupportedException { TestClone testObj = new TestClone(); Object clonedObj = testObj.clone(); // CloneNotSupportedException !! } // ... remainder ingnored }
如果实现了Cloneable接口,同样没有覆盖clone方法,却不再有Exception弹出,换言之,Object.clone()被开放了:
class TestClone implements Cloneable { /* Cloneable implemented, so Object.clone() becomes completely usable */ public void test() throws CloneNotSupportedException { TestClone testObj = new TestClone(); Object clonedObj = testObj.clone(); // Ok, got a cloned object } // ... remainder ingnored }
这个神奇的"开放"过程又是超出Java语言的extralinguistic,因为Object.clone()是native方法,由jvm实现。这个默认的clone方法会对操作对象做逐个属性的浅复制(shadow copy),而且不须调用任何构造函数就返回一个新对象(又extralinguistic了)。
知道了它的运作机制,原本看似简单的clone方法就一下子变复杂了。首先,绝大多数情况下,clone方法必须被覆盖。否则,Object类中默认的clone方法将被调用,这只是一个浅复制,如果被复制的对象带有mutable对象作为属性,那么被复制到新对象中的,将是指向原来属性的引用,所以在新对象中对这一属性进行修改,会同时影响到原对象,反之亦然。比如item 6中的一个例子:
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); } }
如果只把这个类声明为implements Cloneable,而不覆盖clone方法,复制之后,新对象和原对象的elements属性,都会指向内存中同一个数组,因而它们中任何一个对自己的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属性不能是final,否则无法完成重新赋值。这是clone方法的另一个问题:与指向mutable对象属性的final引用有冲突。还有更复杂的情况,如果有属性是Collection类型,而其元素又是mutable对象的引用,那么要对每个元素都调用clone:
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 }
如果数据规模很大,递归调用会有stack overflow风险,所以最好改用循环的架构:
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; }
另一个问题是继承链。对于一个可能被继承的类,其clone方法返回的对象,不能由构造函数来创建,只能由super.clone返回。否则,当其子类调用super.clone方法时,返回的对象将是父类的类型,这是反直觉的,所以super.clone应该像构造函数一样被Object的每一个子类传递调用下去(但编译器却不强制这一点,这导致super.clone的调用链在现实中可能不被保持),这就要求每个父类都提供一个符合规范的clone方法,这很难做到。
还有一个多态调用的问题。与构造函数类似,在clone方法中不能调用任何其它可能被子类覆盖(非final)的方法。因为,在clone完成之前,新对象可能是不完整的,如果被调用的方法使用了尚未被clone修正的数据,就会破坏clone生成对象的完整性,继而产生不可预测的行为。
总结一下,如果一个类实现了Cloneable接口,那么它必须提供一个符合规范的clone方法,在这个方法中,首先调用super.clone来获得一个对象,然后进行必要的深复制(deep copy)。如果把clone方法声明为公共的(public),就不要抛出CloneNotSupportedException,以方便用户使用。如果一个类是作为父类被定义,那么应尽可能仿照Object类:不实现Cloneable接口,并提供一个受保护(protected)的clone方法。
clone架构是Java语言中很不好用的特性,建议用复制构造函数:
public Yum(Yum yum);
或复制静态factory方法:
public static Yum newInstance(Yum yum);
来替代clone方法。这样做在功能上不会有任何损失,却可以避免应用clone方法时可能产生的种种问题。