好艰深的一节,看了两三遍才完全吃透。

覆盖clone方法要谨慎

Java中一个广为人知的复制对象的方法是clone。这个方法看上去方便好用,但实际上问题和限制都很多,所以不推荐使用。需要复制对象的情况下,应该尽可能使用复制构造函数或静态factory方法。

clone方法最大的问题来自于它的实现机制:Cloneable接口。这个接口严重破坏了Java语言中接口的使用规范,所以被称为"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方法时可能产生的种种问题。