第11条:谨慎地覆盖clone
调用clone()方法需要对象实现Cloneable接口,但是这个接口有着许多缺陷。最主要的缺陷在于,Cloneable接口中不包含任何方法,而Object中的clone方法是protect的,也就是说如果一个类只是继承了Cloneable接口,但是却没有重写clone()方法,那么对于这个类的对象,clone()方法依然是不可用的。
既然Cloneable接口中没有任何方法,那么它有什么作用呢,可以看看Object中的clone()方法:
protected Object clone() throws CloneNotSupportedException {
if (!(this instanceof Cloneable)) {
throw new CloneNotSupportedException("Class " + getClass().getName() +" doesn't implement Cloneable");
}
return internalClone();
}
从上面的代码中可以看出Cloneable接口实际上决定了clone()方法的行为,按书中所说:“这是接口的一种极端非典型的用法,并不值得仿效。”
也就是说如果clone()方法想要实现预想中的效果,就需要遵守一个“相当复杂的,不可实施的,并且基本上没有文档说明的协议”。
但是即使是java.lang.Object规范中的约定内容[JavaSE6]中也存在着许多问题,例如:“不调用构造器”,这一条规定太过强硬,因为行为良好的clone()方法可以调用构造器来创建对象,构造之后在复制内部数据。
在最理想的状况下,如果需要使用clone方法,只需要实现Cloneable接口,并且重写clone()方法即可:
@Override
public Object clone(){
try{
return super.clone();
}catch(CloneNotSupportedException e){
throw new AssertionError();
}
}
但是这种方法只适用于需要浅拷贝的情况。但如果这么做,而且拷贝的类中存在可变的对象,就会导致灾难性的结果。
浅拷贝:浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝:深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。
例如我们希望通过上面的方式clone一个如下的Stack:
public class Stack{
private Object[] elements;
private int size = 0;
//other......
}
这样clone出来的Stack对象与原Stack对象却使用的是同一个elements引用,但是size属性却是相互独立的。这样很快就会发现这两个Stack对象已经无法正常工作。
对于这种情况,我们可以通过深拷贝来避免这种情况的发生,例如上面的Stack类,虽简单的做法就是在elements数组中递归地调用clone:
@Override
public Stack clone(){
try{
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
}catch(CloneNotSupportedException e){
throw new AssertionError();
}
}
克隆复杂对象还有一个方法:
先调用super.clone(),然后把返回对象中的所有域都设为空白状态,再将原对象中各个域的值赋给克隆对象中的相应部分,但是它运行起来通常没有“直接操作对象及其克隆对象的内部状态的clone方法”快。
正是因为 Cloneable 接口有着这么多的问题,可以肯定的说,其他接口都不应该扩展此接口;那些为了继承而设计的类也不应该实现此接口。