本文共 6119 字,大约阅读时间需要 20 分钟。
clone()方法的约定
首先明确的是clone()是object的方法。Cloneable接口没有任何方法,它只起到标识的作用。(java的原型模式有用到)
Cloneable接口的目的是作为对象的一个混合接口,表明这样的对象允许克隆(clone),但是这个接口却没有定义clone(),这是它的缺陷:无法约束子类实现clone()方法。
Object定义了一个受保护的clone()方法。Cloneable虽然没有定义clone()方法,但是却影响了Object.clone()方法的行为:如果一个类实现了Cloneable,调用Object的clone()就会返回该对象的逐域拷贝,否则抛出CloneNotSupportedException。这真是一种非常规的用法,Cloneable接口没有规定实现类的视图,却改变了父类受保护方法的行为。调用clone()会创建并返回对象的拷贝,看看JDK文档中对clone()方法的约定:
(1)x.clone() != x; 克隆对象与原对象不是同一个对象
(2)x.clone().getClass() == x.getClass(); 克隆的是同一类型的对象
(3)x.clone().equals(x) == true,如果x.equals()方法定义恰当的话
注意,上面的三条规则要求不是绝对的,一般来说前两条是必需的,第三个也应该尽量遵守。
实现Cloneable接口的类和其所有超类都必需遵守一个复杂、不可实施、且没有文档说明的协议,由此得到一种语言之外的机制:无需调用构造器就可以创建对象。然而,“不调用构造器”的规定有些僵硬,行为良好的clone()方法可以调用构造器创建对象,比如final类,它不会有子类,所以在它的clone()方法中调用构造器创建对象是一种合理的选择。
使用clone()的规则
“如果你覆盖了非final类中的clone方法,则应该返回一个通过调用super.clone()而得到的对象”,这是使用clone()方法的规则,如果不遵守这条规则,在clone()方法中调用了构造器,那么就会得到错误的类。如代码所示:
类B的clone()方法就不会得到正确的对象,因为super.clone()返回的是使用A的构造器创建的类A的对象。如果类B的clone方法想得到正确的对象,那么A的clone方法应该这样写:
由此,我们可以看出调用super.clone()最终会调用Object类的clone方法,前提是子类的所有超类都遵循了上面的规则,否则无法实施。注意,A和B的clone方法的返回值不必是Object,Java1.5引入了协变返回类型作为泛型,覆盖方法的返回值可以是被覆盖方法返回值的子类。
Cloneable实在是一个失败的接口,它并没有指明实现它的类需要承担哪些责任,通常情况下,实现Cloneable的类应当提供一个功能适当的公有的clone()方法。
浅克隆
克隆出来的对象的所有变量含有与原来的对象相同的值,而对其他对象的引用都指向原来的对象。也就是说,浅克隆仅仅克隆所考虑的对象。Object的clone就是"shallow copy"。如果类的每个域都是基本类型的值,或者是指向不可变对象的引用,那么调用Object.clone()就能得到正确的对象。
通常情况下,我们已经得到了正确的对象,但是如果类里面包含代表序列号或者唯一ID的域,或者创建时间的域,还需要对这些域进行修正。
深克隆
深克隆把引用域所指向的对象也克隆一遍。考虑下面这样一个类:
Person类的friend域不是基本类型,而是指向了可变的对象,这个时候如果调用Object.clone()进行浅克隆,那么克隆出来的对象的friend指向的还是原来的dog,就是说:
Person p = new Person(new Dog("金毛"));
p.clone().friend== p.friend;//true
p.clone().friend.name = "狼狗";
p.friend.name.equals("狼狗");//true,改变克隆对象,却同时更改了原对象
实际上,clone方法是另一种构造器:你必须确保不会伤害到原来的对象。为了使Person的clone方法正确工作,也要对friend进行克隆,最简单的做法就是调用friend.clone():
可是,如果friend域是final的,那么上面的clone()也无法正常工作,因为super.clone()时已经给friend赋一次值了,不能再去修正克隆对象的friend域了。这是个根本问题:clone与引用可变对象的final域的正常用法是不相兼容的!
抛去final域的问题不谈,递归的调用clone()方法就解决问题了吗?问题在于,深克隆要深入到哪一层,是一个不易确定的问题。考虑下面的类:
运行这段代码之后会发现,虽然克隆对象有自己的数组buckets,但是数组中引用的链表与原始对象是一样的,修改克隆对象数组中的链表,原始对象中数组保存的对象也会随之而修改。解决这种问题的方法是在Entry类中增加一个“深度拷贝(deep copy)”方法。
克隆复杂对象还有一种方法,先调用super.clone()得到类型正确的对象,然后把所有域都设置成空白状态,然后调用高层的方法重新产生对象的状态。这种做法会产生一个简单、合理且相当优美的clone方法,运行速度稍慢。
总结
1.Cloneable接口是一个失败的接口,它没有提供clone()方法,却影响了Object.clone()克隆的行为:如果类没有实现Cloneable接口,调用super.clone()方法会得到CloneNotSupportedException。
2.所有实现了Cloneable接口的类都应该提供一个公有的方法覆盖clone(),此公有方法首先调用super.clone(),然后修正域,此公有方法一般不应该声明抛出CloneNotSupportedException。
3.如果为了继承而设计的类不应该实现Cloneable接口,这样可以使子类具有实现或者不实现Cloneable接口的自由,就仿佛它们直接扩展了Object一样。父类没有实现Cloneable接口,也没有覆盖clone(),子类如果实现了Cloneable,在覆盖的clone()中调用super.clone()是可以得到正确对象的。
据说很多专家级程序猿从来都不使用clone()方法。
更好的方法
等等,为了实现clone()方法的功能,有必要这么复杂吗?很少有这种必要。为了实现对象拷贝的更好的方法是提供一个拷贝构造器或者拷贝工厂,它们接受这类的一个对象作为参数。
public Yum( Yum yum);//拷贝构造器
public static YumcopyInstance(Yum yum);//拷贝工厂
转载请注明出处: