@XingdingCAO
2018-02-11T17:12:31.000000Z
字数 10007
阅读 1998
Java
clone
Cloneable
接口意图作为一个混入/特征接口,来告知对象们对于克隆操作的允许。不幸的是,它并未实现它的意图。主要的缺陷在于Cloneable
缺乏一个clone
方法,并且java.lang.Object
的clone
方法是protected
。除了采取反射的方式,不能仅仅因为一个类实现了Cloneable
接口就可以调用clone
方法。即使使用了反射,也可能会失败,因为无法保证这个对象拥有一个可访问的clone
方法。尽管这种方式有着诸多不足,但是它仍被广泛使用。
Java 8中的Cloneable
接口的源码如下:
package java.lang;
/**
* A class implements the <code>Cloneable</code> interface to
* indicate to the {@link java.lang.Object#clone()} method that it
* is legal for that method to make a
* field-for-field copy of instances of that class.
* <p>
* Invoking Object's clone method on an instance that does not implement the
* <code>Cloneable</code> interface results in the exception
* <code>CloneNotSupportedException</code> being thrown.
* <p>
* By convention, classes that implement this interface should override
* <tt>Object.clone</tt> (which is protected) with a public method.
* See {@link java.lang.Object#clone()} for details on overriding this
* method.
* <p>
* Note that this interface does <i>not</i> contain the <tt>clone</tt> method.
* Therefore, it is not possible to clone an object merely by virtue of the
* fact that it implements this interface. Even if the clone method is invoked
* reflectively, there is no guarantee that it will succeed.
*
* @author unascribed
* @see java.lang.CloneNotSupportedException
* @see java.lang.Object#clone()
* @since JDK1.0
*/
public interface Cloneable {
}
从注释中也能读出,上述的那些缺点,以及该接口的用处:“indicate to the {@link java.lang.Object#clone()} method that it is legal for that method to make a field-for-field copy of instances of that class”。也就是说,该接口仅作声明用,决定着java.lang.Object
的clone
方法的实现。
正如第二段注释所述,如果一个类实现了该接口,然后调用clone
方法,则会返回一个逐个字段复制的对象;而没有实现该接口的类,在调用时则会抛出CloneNotSupportedException
。这是一种高度非典型的接口使用方法,没有人会效仿!通常情况下,实现一个接口意味着一个类所能为客户端做的事情,而Cloneable
却用来调整一个父类的保护方法的行为。
如果实现Cloneable
接口对一个类有着任何影响,这个类以及它的父类们必须遵循一个相当复杂、非强制确保的、未被详细记录的协议。导致带来的机制是超乎语言本身的——无需调用构造方法就能创建一个对象。
java.lang.clone
的clone
方法有着脆弱的协议(并不严格要求),下面是从Java 8的API截取的协议:
这个协议有着多个问题:
1.克隆的对象创建不会调用构造方法。
一个行为良好的clone
方法可以调在 创建过程中,使用构造方法来创建克隆的内部,甚至在当类被final
修饰时直接返回由构造方法创建的对象(即:在一个继承链上的各个类在重写clone
方法时,都去调用自己的构造方法来构成最终产生的克隆对象,而链条上的最后一个被final
修饰的类,则返回由调用自己的构造方法产生的对象)。
如下的代码进行了一个小的测试,结果输出了相同的两个长整型数。
public class Entity implements Cloneable{
private final long uid;
private Entity(long uid){
this.uid = uid;
}
public long getUid() {
return uid;
}
public static Entity newInstance(){
return new Entity(System.currentTimeMillis());
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Entity entity = Entity.newInstance();
System.out.println(entity.getUid());
Entity newEntity = (Entity) entity.clone();
System.out.println(newEntity.getUid());
}
}
2.上面的协议中的第二条:
这条协议十分脆弱。实际中,程序员假定:如果他们继承了一个类,然后在类中调用了super.clone()
,那么返回的将是父类的一个实例。父类实现此种假定的唯一途经是返回在父类内调用super.clone()
获取到的实例。如果一个clone
方法返回了由构造方法创建的对象,那最终产生的对象会拥有错误的class
。
例如下面的
Entity
与SpecialEntity
正确地调用了super.clone()
:
public class Entity implements Cloneable{
//...
@Override
protected Object clone() throws CloneNotSupportedException {
Object obj = super.clone();
System.out.println(obj.getClass());
return obj;
}
}
public class SpecialEntity extends Entity {
//...
@Override
protected Object clone() throws CloneNotSupportedException {
Object obj = super.clone();
System.out.println(obj.getClass());
return obj;
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException > {
SpecialEntity entity = new SpecialEntity(System.currentTimeMillis());
Object newEntity = entity.clone();
}
}
输出如下(实例可以转换为继承链条上的每一个类的类型):
但是,如果继承链条上有一个类返回了调用构造方法产生的对象,那么该类的子类们将调用
super.clone()
产生的实例类型将停留在该类的类型。
因此,如果在一个未被final
修饰的类重写了clone
方法,应该返回通过调用super.clone()
产生的对象。如果所有的在继承链的类都正确地调用了super.clone()
,那么最终会调用Object
的clone
方法,然后创建继承链末端类的实例。这个过程与自动的构造方法链十分相似,除了前者不是强制的、容易出错的。
3.Cloneable
接口并没有详细地讲述实现该接口的职责。实际上,一个类实现了Cloneable
接口应提供一个恰当的、public
的clone
方法。如果没有,尽可能这样做,除非所有的该类的父类提供了恰当的clone
方法的实现(public
或protected
的)。
假定你想要实现Cloneable
,而且这个类的父类提供了恰当的clone
方法。通过调用super.clone()
返回的对象可能接近,也可能不接近于你最终返回的对象。接近与否由类的“天性”决定。
假设一个类的父类完成遵循了上述的几个要点,正确地重写了clone
方法。这时,在这个类中调用super.clone()
得到的对象会和调用该方法的对象拥有相同的字段值。如果这些字段是原始类型或对不变类的引用,那么克隆所得的对象就是你期待的那样,无需采取进一步的操作。如下面的PhoneNumber
类需要做的就是声明实现Cloneable
接口,以此来提供公开的对Object
中protected
的clone
的方法的访问。
public final class PhoneNumber{
private final short areaCode;
private final short prefix;
private final short lineNumber;
@Override
protected PhoneNumber clone(){
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); //Can't Happen.
}
}
注意,返回类型不是Object
,而是Object
的子类PhoneNumber
。从Java 1.5发行版起,这样的行为是允许的,而且是应该的,因为covariant return types
(协变的返回类型)从Java 1.5被作为泛型的一部分而引入。也就是说,子类重写的方法可以返回类型可以是父类返回类型的子类型,以此来提供更详细的信息,以及消除客户端类型转换的必要。大体上的原则是这样的:能让服务端为客户端做的事情,别交给客户端去做。
如果一个对象包含对可变类的引用,简单地调用上述的clone
方法已经不再有效了。例如,下面的Stack
类:
public class Stack{
private static int DEFAULT_INITIAL_CAPACITY=16;
private Object[] elements;
prvate int size=0;
public Stack(){elements=new Object[DEFAULT_INITIAL_CAPACITY];
public void push(Object obj){
ensureCapacity();
elements[size++]=obj;
}
public Object pop{
if(size==0)
throw new StackEmptyException();
Object result = elements[--size];
elements[size] = null; //消除过时的引用
return result;
}
private void ensureCapacity(){
if(size==elements.length)
elements=Array.copyOf(elements,2*size+1);
}
}
假如你想要让这个类实现Cloneabale
接口,如果返回super.clone()
的结果,那么克隆将会有与原对象相同的size
字段值,但是elements
字段将会引用与原对象相同的数组。对原对象的修改会更改克隆对象的不变性,反过来也是这样。你很快会发现你的程序产生了毫无意义的结果或者抛出NullPointerException
。
这种情形在Stack
类中(在重写的clone
方法中)调用仅有的构造方法时不会发生。实际上,clone
方法的作用就像另一种构造方法;你必须确保它不损害原对象,并且正确地在克隆对象上建立不变性。
为了使Stack
类上的clone
方法正确地工作,它必须复制栈的内部。最简单的方法就是循环地在数组上调用clone
方法。
@Override
public Stack clone(){
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError(); //Can't Happen.
}
}
注意elements.clone()
并没有强制转换为Object[]
。自Java 1.5起,在数组上调用clone
方法将会返回一个编译时类型与原数组一致的数组。Java 8的API文档如下:
在数组调用
clone
方法,不过是为数组分配了一个新的引用。例如:
Entity[] array = { Entity.newInstance(), Entity.newInstance(), Entity.newInstance()};
System.out.println(array+":"+Arrays.toString(array));
Entity[] newArray = array.clone(); //等效于Arrays.copyOf(array,array.length)
System.out.println(newArray+":"+Arrays.toString(newArray));
结果为:
可以看出,克隆的数组引用有着与原引用不同的地址,但是数组内容相同。
还需要注意的是,上述的情况在当elements
字段被final
修饰是行不通的,因为elements
在对象创建完成后不允许再次被赋值。这是一个基本的问题:clone
结构与正常使用的引用可变对象的final
字段不兼容,除非可变对象可以安全地被原对象和克隆对象共享。如果想要使一个类实现CLoneable
接口,那么必须在某些字段上移除final
修饰符。
有时候,仅仅递归地调用clone
方法是不够的。例如,假定你在为一个哈希表编写clone
方法,哈希表内部包含一个数组,数组的每个元素引用着类似链表的第一个键值对入口,当元素为null
时代表链表为空。为了性能,该类实现了自有的、轻量的单链表,而没有去实现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;
}
}
//...
}
假定你仅仅克隆了buckets
数组,就像之前对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);
}
}
@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
}
私有类Entry
被扩充了一个名为deepCopy
的方法。clone
方法分配了一个全新的buckets
数组,该数组拥有着正确的大小,并且便利了原数组,深度复制了每一个非空的链表。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()
,并且将所有字段设为初始状态,然后调用高层次的方法来重铸对象的状态。在HashTable
的例子中,buckets
字段将会初始化为一个新的数组,然后调用put(key,value)
来把每个键值对放入克隆的哈希表中。这种方法描绘一个简单、合理的、优雅的clone
方法,但通常没有直接操作对象与其克隆对象的内部那样高效。
就像构造方法,在创建时,clone
方法不应该在克隆对象上调用任何有可能被子类重写的方法。如果clone
方法调用了一个重写过的方法,这个方法的将会在重写者有机会调整克隆对象的状态之前执行,非常有可能导致原对象以及克隆对象的崩溃。因此,刚才说的HashTable
的例子中的put(key,value)
方法应该被final
或private
修饰。
Object
的clone
方法声明会抛出CloneNotSupportedException
,但是重写时应该去掉它,因为一个不抛出checked exception(受检查异常)的类更易使用。如果一个类被用于继承,且重写了clone
方法,那么重写的方法应该模仿Object.clone
方法:应该声明为protected
的,,它应该声明会抛出CloneNotSupportedException
,并且不应该实现Cloneable
接口。这样的做法给了子类是否实现Cloneable
接口的自由,就好像子类直接继承Object
类一样。
还有一件事,实在不能忍。如果你决定,创建一个实现Cloneable
接口的线程安全的类,记住:clone
方法就像其他方法一样需要被正确加上同步锁。Object
的clone
方法并不是同步的,所以即使其他一切都满足了,你也得写一个同步的clone
方法(调用super.clone
)。
重述一遍,所有实现了Cloneable
接口的类都应该使用一个public
的返回类型为类自身的方法重写clone
方法。这个方法首先应该调用super.clone
,然后修正需要修正的字段。通常,这就意味着复制可变对象组成了待克隆对象的深层次结构。而进一步的操作将这些简单复制的引用替换为真正克隆了的对象引用。尽管这些内部复制通常可以通过递归地调用clone
来实现,但这并不是最佳的解决方案。如果一个类仅含有对原始类型和不变类引用的字段,那么不再需要额外的操作。但是,这条规则也有些例外。例如,一个字段代表了一个序列号、唯一ID,又或者一个字段代表了对象的创建时间,那么这些例外就需要额外的操作来修正。
所有的这些复杂性都是必须的吗?少之又少。
如果你继承了一个实现了Cloneable
接口的类,那么你别无选择,只能去实现了一个恰当的clone
方法。除此之外,你最好为对象的复制提供一个额外的选项,又或者 不去提供克隆的能力。例如,一个不变类去支持对象克隆并没有什么道理,因为产生的克隆事实上难以与原对象区分。
良好的对象克隆的方法是提供一个拷贝构造方法或者拷贝工厂。拷贝构造方法是一个简单的、仅有一个参数的构造方法,唯一的参数类型为构造方法所在的类的类型。例如:
public Yum(Yum yum);
一个拷贝工厂就是与拷贝构造方法类似的、静态工厂的方法。
public static Yum newInstance(Yum yum);
拷贝构造方法和静态工厂方法有着Cloneable/clone
无可比拟的优势:
final
字段的使用不冲突尽管拷贝构造方法或拷贝工厂方法不能放入一个接口中,但是Cloneable
接口也没有像一个接口似的工作,因为它缺少一个公有的clone
方法。因此,你并非放弃了接口的功能,而是选择了优于现存的clone
方法的拷贝构造方法或拷贝工厂方法。
更难得的是,拷贝构造方法或拷贝工厂方法可以接收所在类实现了的某一接口类型的一个参数。例如,按照惯例,所有通常的集合实现类提供了一个参数类型为Collection
或Map
的构造方法。
例如
java.util.LinkedList
就有着上述的构造方法:
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
这种基于接口的拷贝构造方法或工厂方法,通常被称作"conversion constructors"或"conversion factories"。这种做法留给了客户端选择克隆的实现类的权利,而不用强制接受特定的原对象的实现类型。例如,现在有一个Hashset
类型的s
,然后你想要一个TreeSet
类型的克隆,那么clone
方法没法满足你,但是使用一个"conversion constructor"可以轻松实现:new TreeSet(s)
。
给出了所有的这些与Cloneable
接口有关的问题后,有理由这样说:其他接口不应该继承这个接口,并且所有被用来继承的类不应该实现此接口。由于众多的缺点,一些编程专家简单地选择永远不去重写clone
方法,也不会去调用它,除了在数组上调用。如果你设计了一个用于继承的类,明白这一点:如果你选择不去提供一个恰当的protected
的clone
方法,那么子类将失去实现Cloneable
接口的可能性。