[关闭]
@XingdingCAO 2018-02-11T09:12:31.000000Z 字数 10007 阅读 1821

Effective Java(第2版):item 11 —— Override clone judiciously.

Java clone


Cloneable接口意图作为一个混入/特征接口,来告知对象们对于克隆操作的允许。不幸的是,它并未实现它的意图。主要的缺陷在于Cloneable缺乏一个clone方法,并且java.lang.Objectclone方法是protected。除了采取反射的方式,不能仅仅因为一个类实现了Cloneable接口就可以调用clone方法。即使使用了反射,也可能会失败,因为无法保证这个对象拥有一个可访问的clone方法。尽管这种方式有着诸多不足,但是它仍被广泛使用。

Cloneable有什么用?

Java 8中的Cloneable接口的源码如下:

  1. package java.lang;
  2. /**
  3. * A class implements the <code>Cloneable</code> interface to
  4. * indicate to the {@link java.lang.Object#clone()} method that it
  5. * is legal for that method to make a
  6. * field-for-field copy of instances of that class.
  7. * <p>
  8. * Invoking Object's clone method on an instance that does not implement the
  9. * <code>Cloneable</code> interface results in the exception
  10. * <code>CloneNotSupportedException</code> being thrown.
  11. * <p>
  12. * By convention, classes that implement this interface should override
  13. * <tt>Object.clone</tt> (which is protected) with a public method.
  14. * See {@link java.lang.Object#clone()} for details on overriding this
  15. * method.
  16. * <p>
  17. * Note that this interface does <i>not</i> contain the <tt>clone</tt> method.
  18. * Therefore, it is not possible to clone an object merely by virtue of the
  19. * fact that it implements this interface. Even if the clone method is invoked
  20. * reflectively, there is no guarantee that it will succeed.
  21. *
  22. * @author unascribed
  23. * @see java.lang.CloneNotSupportedException
  24. * @see java.lang.Object#clone()
  25. * @since JDK1.0
  26. */
  27. public interface Cloneable {
  28. }

从注释中也能读出,上述的那些缺点,以及该接口的用处:“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.Objectclone方法的实现。

正如第二段注释所述,如果一个类实现了该接口,然后调用clone方法,则会返回一个逐个字段复制的对象;而没有实现该接口的类,在调用时则会抛出CloneNotSupportedException。这是一种高度非典型的接口使用方法,没有人会效仿!通常情况下,实现一个接口意味着一个类所能为客户端做的事情,而Cloneable却用来调整一个父类的保护方法的行为。

如果实现Cloneable接口对一个类有着任何影响,这个类以及它的父类们必须遵循一个相当复杂、非强制确保的、未被详细记录的协议。导致带来的机制是超乎语言本身的——无需调用构造方法就能创建一个对象。

clone方法

协议

java.lang.cloneclone方法有着脆弱的协议(并不严格要求),下面是从Java 8的API截取的协议:
clone

这个协议有着多个问题:

1.克隆的对象创建不会调用构造方法。

一个行为良好的clone方法可以调在 创建过程中,使用构造方法来创建克隆的内部,甚至在当类被final修饰时直接返回由构造方法创建的对象(即:在一个继承链上的各个类在重写clone方法时,都去调用自己的构造方法来构成最终产生的克隆对象,而链条上的最后一个被final修饰的类,则返回由调用自己的构造方法产生的对象)。

如下的代码进行了一个小的测试,结果输出了相同的两个长整型数。

  1. public class Entity implements Cloneable{
  2. private final long uid;
  3. private Entity(long uid){
  4. this.uid = uid;
  5. }
  6. public long getUid() {
  7. return uid;
  8. }
  9. public static Entity newInstance(){
  10. return new Entity(System.currentTimeMillis());
  11. }
  12. @Override
  13. public Object clone() throws CloneNotSupportedException {
  14. return super.clone();
  15. }
  16. }
  17. public class Main {
  18. public static void main(String[] args) throws CloneNotSupportedException {
  19. Entity entity = Entity.newInstance();
  20. System.out.println(entity.getUid());
  21. Entity newEntity = (Entity) entity.clone();
  22. System.out.println(newEntity.getUid());
  23. }
  24. }

2.上面的协议中的第二条:

getClass

这条协议十分脆弱。实际中,程序员假定:如果他们继承了一个类,然后在类中调用了super.clone(),那么返回的将是父类的一个实例。父类实现此种假定的唯一途经是返回在父类内调用super.clone()获取到的实例。如果一个clone方法返回了由构造方法创建的对象,那最终产生的对象会拥有错误的class

例如下面的EntitySpecialEntity正确地调用了super.clone()

  1. public class Entity implements Cloneable{
  2. //...
  3. @Override
  4. protected Object clone() throws CloneNotSupportedException {
  5. Object obj = super.clone();
  6. System.out.println(obj.getClass());
  7. return obj;
  8. }
  9. }
  1. public class SpecialEntity extends Entity {
  2. //...
  3. @Override
  4. protected Object clone() throws CloneNotSupportedException {
  5. Object obj = super.clone();
  6. System.out.println(obj.getClass());
  7. return obj;
  8. }
  9. }
  1. public class Main {
  2. public static void main(String[] args) throws CloneNotSupportedException > {
  3. SpecialEntity entity = new SpecialEntity(System.currentTimeMillis());
  4. Object newEntity = entity.clone();
  5. }
  6. }

输出如下(实例可以转换为继承链条上的每一个类的类型):
super.clone

但是,如果继承链条上有一个类返回了调用构造方法产生的对象,那么该类的子类们将调用super.clone()产生的实例类型将停留在该类的类型。

因此,如果在一个未被final修饰的类重写了clone方法,应该返回通过调用super.clone()产生的对象。如果所有的在继承链的类都正确地调用了super.clone(),那么最终会调用Objectclone方法,然后创建继承链末端类的实例。这个过程与自动的构造方法链十分相似,除了前者不是强制的、容易出错的。

3.Cloneable接口并没有详细地讲述实现该接口的职责。实际上,一个类实现了Cloneable接口应提供一个恰当的、publicclone方法。如果没有,尽可能这样做,除非所有的该类的父类提供了恰当的clone方法的实现(publicprotected的)。

"shadow copy" 还是 "deep copy" ?

无需采取更多操作的情形

假定你想要实现Cloneable,而且这个类的父类提供了恰当的clone方法。通过调用super.clone()返回的对象可能接近,也可能不接近于你最终返回的对象。接近与否由类的“天性”决定。

假设一个类的父类完成遵循了上述的几个要点,正确地重写了clone方法。这时,在这个类中调用super.clone()得到的对象会和调用该方法的对象拥有相同的字段值。如果这些字段是原始类型或对不变类的引用,那么克隆所得的对象就是你期待的那样,无需采取进一步的操作。如下面的PhoneNumber类需要做的就是声明实现Cloneable接口,以此来提供公开的对Objectprotectedclone的方法的访问。

  1. public final class PhoneNumber{
  2. private final short areaCode;
  3. private final short prefix;
  4. private final short lineNumber;
  5. @Override
  6. protected PhoneNumber clone(){
  7. try {
  8. return (PhoneNumber) super.clone();
  9. } catch (CloneNotSupportedException e) {
  10. throw new AssertionError(); //Can't Happen.
  11. }
  12. }

注意,返回类型不是Object,而是Object的子类PhoneNumber。从Java 1.5发行版起,这样的行为是允许的,而且是应该的,因为covariant return types(协变的返回类型)从Java 1.5被作为泛型的一部分而引入。也就是说,子类重写的方法可以返回类型可以是父类返回类型的子类型,以此来提供更详细的信息,以及消除客户端类型转换的必要。大体上的原则是这样的:能让服务端为客户端做的事情,别交给客户端去做

采取必要措施的情形

如果一个对象包含对可变类的引用,简单地调用上述的clone方法已经不再有效了。例如,下面的Stack类:

  1. public class Stack{
  2. private static int DEFAULT_INITIAL_CAPACITY=16;
  3. private Object[] elements;
  4. prvate int size=0;
  5. public Stack(){elements=new Object[DEFAULT_INITIAL_CAPACITY];
  6. public void push(Object obj){
  7. ensureCapacity();
  8. elements[size++]=obj;
  9. }
  10. public Object pop{
  11. if(size==0)
  12. throw new StackEmptyException();
  13. Object result = elements[--size];
  14. elements[size] = null; //消除过时的引用
  15. return result;
  16. }
  17. private void ensureCapacity(){
  18. if(size==elements.length)
  19. elements=Array.copyOf(elements,2*size+1);
  20. }
  21. }

假如你想要让这个类实现Cloneabale接口,如果返回super.clone()的结果,那么克隆将会有与原对象相同的size字段值,但是elements字段将会引用与原对象相同的数组。对原对象的修改会更改克隆对象的不变性,反过来也是这样。你很快会发现你的程序产生了毫无意义的结果或者抛出NullPointerException

这种情形在Stack类中(在重写的clone方法中)调用仅有的构造方法时不会发生。实际上,clone方法的作用就像另一种构造方法;你必须确保它不损害原对象,并且正确地在克隆对象上建立不变性。

为了使Stack类上的clone方法正确地工作,它必须复制栈的内部。最简单的方法就是循环地在数组上调用clone方法。

  1. @Override
  2. public Stack clone(){
  3. try {
  4. Stack result = (Stack) super.clone();
  5. result.elements = elements.clone();
  6. return result;
  7. } catch (CloneNotSupportedException e) {
  8. throw new AssertionError(); //Can't Happen.
  9. }
  10. }

注意elements.clone()并没有强制转换为Object[]。自Java 1.5起,在数组上调用clone方法将会返回一个编译时类型与原数组一致的数组。Java 8的API文档如下:cloneArray

在数组调用clone方法,不过是为数组分配了一个新的引用。例如:

  1. Entity[] array = { Entity.newInstance(), Entity.newInstance(), Entity.newInstance()};
  2. System.out.println(array+":"+Arrays.toString(array));
  3. Entity[] newArray = array.clone(); //等效于Arrays.copyOf(array,array.length)
  4. System.out.println(newArray+":"+Arrays.toString(newArray));

结果为:
array

可以看出,克隆的数组引用有着与原引用不同的地址,但是数组内容相同。

还需要注意的是,上述的情况在当elements字段被final修饰是行不通的,因为elements在对象创建完成后不允许再次被赋值。这是一个基本的问题:clone结构与正常使用的引用可变对象的final字段不兼容,除非可变对象可以安全地被原对象和克隆对象共享。如果想要使一个类实现CLoneable接口,那么必须在某些字段上移除final修饰符。

有时候,仅仅递归地调用clone方法是不够的。例如,假定你在为一个哈希表编写clone方法,哈希表内部包含一个数组,数组的每个元素引用着类似链表的第一个键值对入口,当元素为null时代表链表为空。为了性能,该类实现了自有的、轻量的单链表,而没有去实现java.util.LinkedList

  1. public class HashTable implements Cloneable{
  2. private Entry[] buckets = ...;
  3. private static class Entry{
  4. final Object key;
  5. Object value;
  6. Entry next;
  7. Entry(Object key, Object value, Entry next){
  8. this.key = key;
  9. this.value = value;
  10. this.next = next;
  11. }
  12. }
  13. //...
  14. }

假定你仅仅克隆了buckets数组,就像之前对Stack类做的。

  1. // Broken - results in shared internal state!
  2. @Override
  3. public HashTable clone() {
  4. try {
  5. HashTable result = (HashTable) super.clone();
  6. result.buckets = buckets.clone(); return result;
  7. } catch (CloneNotSupportedException e) {
  8. throw new AssertionError();
  9. }
  10. }

虽然克隆有着自己的数组引用,但是数组引用着的链表与原对象相同,这样会在原对象和克隆对象上导致诸多不确定的行为。为了改正这个问题,你必须单独地复制内部的链表。

  1. public class HashTable implements Cloneable {
  2. private Entry[] buckets = ...;
  3. private static class Entry{
  4. final Object key;
  5. Object value;
  6. Entry next;
  7. Entry(Object key, Object value, Entry next){
  8. this.key = key;
  9. this.value = value;
  10. this.next = next;
  11. }
  12. // Recursively copy the linked list headed by this Entry
  13. Entry deepCopy(){
  14. return new Entry(key, value, next==null?null:next.deepCopy);
  15. }
  16. }
  17. @Override
  18. public HashTable clone() {
  19. try {
  20. HashTable result = (HashTable) super.clone();
  21. result.buckets = new Entry[buckets.length];
  22. for (int i = 0; i < buckets.length; i++)
  23. if (buckets[i] != null)
  24. result.buckets[i] = buckets[i].deepCopy();
  25. return result;
  26. } catch (CloneNotSupportedException e) {
  27. throw new AssertionError();
  28. }
  29. }
  30. ... // Remainder omitted
  31. }

私有类Entry被扩充了一个名为deepCopy的方法。clone方法分配了一个全新的buckets数组,该数组拥有着正确的大小,并且便利了原数组,深度复制了每一个非空的链表。Entry类的深度复制方法递归地复制了整个链表。这种方法很不错并且管用如果链表不是很长,因为每一个结点就要消耗一个栈结构。如果链表过长,那么很容易造成栈溢出。为了阻止溢出的发生,你可以用非递归的方法取代原复制方法。

  1. Entry deepCopy(){
  2. Entry result = new Entry(key, value, next);
  3. for (Entry p = result; p.next != null; p = p.next)
  4. p.next = new Entry(p.next.key, p.next.value, p.next.next);
  5. return result;
  6. }

正确的做法

最终克隆一个复杂对象的解决方法就是调用super.clone(),并且将所有字段设为初始状态,然后调用高层次的方法来重铸对象的状态。在HashTable的例子中,buckets字段将会初始化为一个新的数组,然后调用put(key,value)来把每个键值对放入克隆的哈希表中。这种方法描绘一个简单、合理的、优雅的clone方法,但通常没有直接操作对象与其克隆对象的内部那样高效。

就像构造方法,在创建时,clone方法不应该在克隆对象上调用任何有可能被子类重写的方法。如果clone方法调用了一个重写过的方法,这个方法的将会在重写者有机会调整克隆对象的状态之前执行,非常有可能导致原对象以及克隆对象的崩溃。因此,刚才说的HashTable的例子中的put(key,value)方法应该被finalprivate修饰。

Objectclone方法声明会抛出CloneNotSupportedException,但是重写时应该去掉它,因为一个不抛出checked exception(受检查异常)的类更易使用。如果一个类被用于继承,且重写了clone方法,那么重写的方法应该模仿Object.clone方法:应该声明为protected的,,它应该声明会抛出CloneNotSupportedException,并且不应该实现Cloneable接口。这样的做法给了子类是否实现Cloneable接口的自由,就好像子类直接继承Object类一样。

还有一件事,实在不能忍。如果你决定,创建一个实现Cloneable接口的线程安全的类,记住:clone方法就像其他方法一样需要被正确加上同步锁。Objectclone方法并不是同步的,所以即使其他一切都满足了,你也得写一个同步的clone方法(调用super.clone)。

重述一遍,所有实现了Cloneable接口的类都应该使用一个public的返回类型为类自身的方法重写clone方法。这个方法首先应该调用super.clone,然后修正需要修正的字段。通常,这就意味着复制可变对象组成了待克隆对象的深层次结构。而进一步的操作将这些简单复制的引用替换为真正克隆了的对象引用。尽管这些内部复制通常可以通过递归地调用clone来实现,但这并不是最佳的解决方案。如果一个类仅含有对原始类型和不变类引用的字段,那么不再需要额外的操作。但是,这条规则也有些例外。例如,一个字段代表了一个序列号、唯一ID,又或者一个字段代表了对象的创建时间,那么这些例外就需要额外的操作来修正。

思考

所有的这些复杂性都是必须的吗?少之又少。

如果你继承了一个实现了Cloneable接口的类,那么你别无选择,只能去实现了一个恰当的clone方法。除此之外,你最好为对象的复制提供一个额外的选项,又或者 不去提供克隆的能力。例如,一个不变类去支持对象克隆并没有什么道理,因为产生的克隆事实上难以与原对象区分。

更合理的选择

良好的对象克隆的方法是提供一个拷贝构造方法或者拷贝工厂。拷贝构造方法是一个简单的、仅有一个参数的构造方法,唯一的参数类型为构造方法所在的类的类型。例如:

  1. public Yum(Yum yum);

一个拷贝工厂就是与拷贝构造方法类似的、静态工厂的方法。

  1. public static Yum newInstance(Yum yum);

拷贝构造方法和静态工厂方法有着Cloneable/clone无可比拟的优势:

尽管拷贝构造方法或拷贝工厂方法不能放入一个接口中,但是Cloneable接口也没有像一个接口似的工作,因为它缺少一个公有的clone方法。因此,你并非放弃了接口的功能,而是选择了优于现存的clone方法的拷贝构造方法或拷贝工厂方法。

更难得的是,拷贝构造方法或拷贝工厂方法可以接收所在类实现了的某一接口类型的一个参数。例如,按照惯例,所有通常的集合实现类提供了一个参数类型为CollectionMap的构造方法。

例如java.util.LinkedList就有着上述的构造方法:

  1. public LinkedList(Collection<? extends E> c) {
  2. this();
  3. addAll(c);
  4. }

这种基于接口的拷贝构造方法或工厂方法,通常被称作"conversion constructors"或"conversion factories"。这种做法留给了客户端选择克隆的实现类的权利,而不用强制接受特定的原对象的实现类型。例如,现在有一个Hashset类型的s,然后你想要一个TreeSet类型的克隆,那么clone方法没法满足你,但是使用一个"conversion constructor"可以轻松实现:new TreeSet(s)

尾巴

给出了所有的这些与Cloneable接口有关的问题后,有理由这样说:其他接口不应该继承这个接口,并且所有被用来继承的类不应该实现此接口。由于众多的缺点,一些编程专家简单地选择永远不去重写clone方法,也不会去调用它,除了在数组上调用。如果你设计了一个用于继承的类,明白这一点:如果你选择不去提供一个恰当的protectedclone方法,那么子类将失去实现Cloneable接口的可能性。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注