×

消息

EU e-Privacy Directive

This website uses cookies to manage authentication, navigation, and other functions. By using our website, you agree that we can place these types of cookies on your device.

View e-Privacy Directive Documents

You have declined cookies. This decision can be reversed.

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

覆盖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方法时可能产生的种种问题。

提交评论


安全码
刷新