《Effective Java》读书笔记09谨慎地覆盖clone方法

一、Cloneable接口与Object.clone方法

Cloneable接口的目的是作为对象的一个mixin接口(混合型接口),表明这样的对象允许克隆(clone)。遗憾的是Cloneable接口里并没有clone方法,其实它什么方法都没有,跟Serializable接口一样,都是占着茅坑不拉屎。它只是声明该对象可以被克隆,具体行为由类设计者决定。如果类设计者忘记提供一个良好的clone方法或根本不提供clone方法,那么类客户使用时必定会出错,这样Cloneable接口并没达到它的目的。

Cloneable接口决定了Object中受保护的clone方法的行为:如果一个类实现了Cloneable,Object的clone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException异常。这是接口的极端非典型的用法,不值得效仿,因为它违背了接口的使用规范,改变了超类中受保护的方法的行为。

Cloneable接口约束实现类及其所有超类都必须遵守一种语言之外的机制:无需调用构造器就可以创建对象。通常这种机制的通用约定是非常弱的。它要求实现类和父类必须提供一个Clone方法,在Clone方法中调用super.clone方法,已达到最终调用Object.clone方法来完成约定。如果有一层没有按照约定实现,那么该类的Clone功能将是潜在的灾难。

二、Object中Clone方法的通用约定

Clone方法用于创建和返回对象的一个拷贝,一般含义如下:

1、对于任何对象x,表达式 x.clone()!=x 将会是true,并且表达式 x.clone().getClass() == x.getClass()将会是true,但这不是绝对要求。

2、通常情况下,表达式 x.clone.equals(x)将会是true,同1一样这不是绝对要求。

拷贝对象往往会导致创建它的类的一个新实例,但它同时也要求拷贝内部的数据接口,这个过程中没有调用构造器。

该通用约定存在的问题:

1、不调用构造器的规定太强硬

行为良好的clone方法可以调用构造器来创建对象,构造之后再复制内部数据。如果这个类是final的,clone甚至可能会返回一个由构造器创建的对象。既然类是final的,不可变的,我当然可以调用构造器创建一个实例,甚至缓存起来(单例模式),等调用clone时直接返回该对象,这样效率更高。

2、x.clone().getClass()通常应该等同于x.getClass()的规定太软弱

在实践中,我们一般会假设:如果扩展一个类,并在子类中调用了super.clone,返回的对象就将是该子类的实例(我们要克隆的是子类而不是父类)。

超类提供此功能的唯一途径是:返回一个通过调用super.clone而得到的对象。如果clone方法返回一个由构造器创建的对象,它就会得到错误的类(当前父类而不是想要的子类)。

因此,如果你覆盖了非final类中的clone方法,则应该返回一个通过调用super.clone而得到的对象。如果类的所有超类都遵守这条规则,那调用super.clone方法最终会调用Object.clone方法,从而创建正确类的实例,此机制类似于自动的构造器调用链,只不过它不是强制要求的。

三、实现一个行为良好的clone方法

从super.clone()中得到的对象有时接近于最终要返回的对象,有时会相差很远,这取决于该类的本质。

1、每个域包含的只有基本类型或指向不可变对象的引用,这种情况返回的对象可能满足我们的需要,比如《读书笔记08》中的PhoneNumber类。在此,我们只需声明实现Cloneable接口,然后对Object中受保护的clone方法提供公有的访问途径:

 @Override public PhoneNumber clone() {
        try {
            return (PhoneNumber) super.clone();//协变返回类型,永远不要让客户去做任何类库能够替他完成的事情。
        } catch(CloneNotSupportedException e) {
            throw new AssertionError();  // Can't happen
        }
    }

2、域中包含可变对象,如《读书笔记05》中的Stack类。

如果想把该类做成cloneable的,如果它的clone方法仅仅返回super.clone(),这样得到的Stack实例虽然size域具有正确的值(基本类型),但它的elements域将引用与原始Stack实例相同的数组。修改原始的实例会破坏被克隆对象中的约束条件,反之亦然。

clone方法就是另一个构造器,你必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件。

修改版:

 @Override public Stack clone() {
        try {
            Stack result = (Stack) super.clone();
            result.elements = elements.clone();//递归调用clone。如果elements是final的,则需要把final去掉,因为final使得elements域不能被赋新值。另外,在数组上调用clone返回是数组,并且它的编译时类型与被克隆数组的类型相同
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }

3、变量中的变量之深度拷贝

有时候递归地调用 clone还不够。比如,自己实现一个散列表并为它编写clone方法,它的内部数据包含一个散列桶数组,每个散列桶都指向"键-值"对链表的第一个项,如果桶是空的,则为null。出于性能方面的考虑,该类实现了自己的轻量级单向链表,而没有使用java内部的java.util.LinkedList,具体类实现如下:

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;
	}
	....//Remainder omitted
}

假如我们仅仅像对Stack那样递归地克隆这个散列桶数组,如下:

	//Broken - results in shared internal state!
	@Override public HashTable clone(){
		try{
			HashTable result = (HashTable) super.clone();
			result.buckets = buckets.clone();
			return result;
		}catch(CloneNotSupportedException e){
			throw new AssertionError();
		}
	}

虽然被克隆的对象有它自己的散列桶数组,但这个数组引用的链表与原始对象是一样的,从而容易引起克隆对象和原始对象中不确定的行为。为修正该问题,需要单独地拷贝并组成每个桶的链表,下面是一种常用做法:

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());
	} }
	//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();
		}
	}
	
}

私有类HashTable.Entry被加强了,支持深度拷贝。此方法虽然很灵活,但如果链表比较长,则很容易导致栈溢出,列表中的每个元素都要消耗一段栈空间的。可以采用迭代来代替递归,如下:

//Iteratively copy the linked list headed by this Entry
	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;
	}

克隆复杂对象的最后一种办法:先调用super.clone,然后把结果对象中的所有域都设置成它的空白状态,然后调用高层(higher-level)的方法来重新产生对象的状态。这种方式简单,合理且优美,但运行速度通常没有"直接操作对象及其克隆对象的内部状态的clone方法"快。

四、clone方法的替代品

如果我们扩展一个实现了Cloneable接口的类,那么除了实现一个行为良好的clone方法外,没有别的选择。否则,最好提供某些其他的途径来代替对象拷贝,或者干脆不提供这样的功能。

实现对象拷贝的好方法:

提供一个拷贝构造器或拷贝工厂

//Copy constructor
public Yum(Yum yum);
//Copy factory
public static Yum newInstance(Yum yum);

此方式优点:

1、它们不依赖于某一种很有风险的,语言之外的对象创建机制。

2、它们不要求遵守尚未制定好的文档规范。

3、它们不会与final的正常使用发生冲突。

4、它们不会抛出不必要的受检异常。

5、它们不需要进行类型转换。

6、它们可以带一个参数,参数类型是通过该类实现的接口,比如集合框架。

五、最佳编程实践

如果必须提供clone方法:

1、clone方法不应该在构造的过程中,调用新对象中任何非final的方法,会造成克隆对象与原始对象的状态不一致。

2、公有的clone方法应该省略CloneNotSupportException异常,因为这样使用起来更轻松。如果专门为了继承而设计的类覆盖了clone方法,覆盖版本的clone方法就应该模拟Object.clone的行为:它应该被声明为protected,抛出CloneNotSupportException异常,并且该类不应该实现Cloneable接口,以便子类可以自己决定是否实现它。

3、用线程安全的类实现Cloneable接口,要记得它的clone方法必须得到很好地同步。

4、任何实现了Cloneable接口的类都应该用一个公有的方法覆盖clone。此方法首先调用super.clone,然后修正任何需要修正的域。

5、使用拷贝构造器或拷贝工厂来代替clone方法

原文地址:https://www.cnblogs.com/xinyuyuanm/p/3003980.html