EffectiveJava——对于所有对象都通用的方法2

第11条:谨慎的覆盖clone

实现Cloneable接口,表明对象是可以被克隆的,它决定Object中受保护方法cloen实现的行为,如果实现了Cloneable,Object的clone方法返回该对象的逐域拷贝,否则抛出CloneNotSupportException异常。

这是接口的极端用法,不值得效仿

规范

clone方法不会调用对象的构造方法
clone方法返回一个该对象的拷贝,这个拷贝的精确含义取决于该对象的类,一般含义是:

对于任意的对象x,表示式:
   x.clone() != x;
将会是true,并且表达式
   x.clone().getCass() == x.getClass()
将会是ture,但是这都不是绝对的,虽然通常情况下,表达式
   x.clone().equals(x)
将会是true,但是也不是绝对的

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

从super.clone()中得到的对象可能接近于最重要返回的对象,也可能相差甚远,这取决于这个类的本质。

示例

如果对象的每个域包含一个基本类型的值,或者包含一个指向不可变对象的引用,那么返回的对象则可能正式你所需要的对象,这种情况不需要再做处理,如前面章节的示例PhoneNumber:

     @Override
        public PhoneNumber clone() {
            try {
                return (PhoneNumber) super.clone();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
                throw new AssertionError();//不会发生
            }
        }

clone返回的类型是PhoneNumber,而不是Object,从Java1.5开始这么做是合法的。

如果对象中包含的域引用了可变对象,使用上面简单的clone方法可能造成灾难性的结果,如下Stack类:

public static class Stack implements Cloneable {
        private Object[] objects;
        private int size;
        public static final int DEFAULT_SIZE = 16;

        public Stack() {
            objects = new Object[DEFAULT_SIZE];
        }


        public void push(Object o) {
            ensureCapacity();
            objects[size++] = o;
        }

        public Object pop() {
            if (size == 0) {
                throw new EmptyStackException();
            }
            Object o = objects[--size];
            objects[size] = null;
            return o;
        }

        private void ensureCapacity() {
            if (size == objects.length) {
                objects = Arrays.copyOf(objects, 2 * size + 1);
            }
        }

        //just for test
        public Object get() {
            return objects;
        }

        @Override
        public Stack clone() {
            try {
                return (Stack) super.clone();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
                throw new AssertionError();
            }
        }
    }

我们在客户端调用:

  Stack stack = new Stack();
        stack.push(12);
        Stack stack1 = stack.clone();
        System.out.println(stack.get() == stack1.get());

结果是true

这表明被clone的Stack1对于所引用的objects与原Stack所引用的objects是相同的数据实例,修改原始实例会破坏被克隆对象中的约束条件,反之亦然。

实际上clone方法就是另一个构造器,必须确保它不会伤害到原始的对象,并确保正确的创建被克隆,为了使Stack能够正常工作,它必须要拷贝栈的内部信息。如:

      @Override
        public Stack clone() {
            try {
                Stack result =  (Stack) super.clone();
                result.objects = objects.clone();
                return result;
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
                throw new AssertionError();
            }
        }

同时我们还需要注意,如果elements域是final的,上面方案就不能正常工作,因为clone方法是禁止给final域赋新值的,clone框架与引用可比变对象的fianl域的正常用法是不相兼容的,除非原始对象可与被克隆对象之间安全的共享此可变对象。为了使类成为可以克隆的,可能有必要从某些域中去掉final修饰符。

但是对付更加复杂的对象上面方式实现的clone方法可能还不够,比如HashTable这样的类:

    public static class HashTable implements Cloneable{

        private Entry[] buckets = ...;

        private static class Entry{
            final Object key;
            Object value;
            Entry next;

            public Entry(Object key, Object value, Entry next) {
                this.key = key;
                this.value = value;
                this.next = next;
            }
        }
    }

虽然被克隆的对象有它自己的散列桶数组,但是这个数组引用的链表与原始的对象是一样的,从而很容易引起克隆对象和原始对象的不确定行为。为了修复这个问题,我们必须单独的拷贝并组成每个桶的链表。如:

public static class HashTable implements Cloneable {

        private Entry[] buckets =  ...;

        @Override
        protected HashTable clone() {
            try {
                HashTable result = (HashTable) super.clone();
                result.buckets = new Entry[buckets.length];
                int length = buckets.length;
                for (int i = 0; i < length; i++) {
                    if (buckets[i] != null) {
                        result.buckets[i] = buckets[i].deepCopy();
                    }
                }
                return result;
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
        }

        private static class Entry {
            final Object key;
            Object value;
            Entry next;

            Entry deepCopy() {
                return new Entry(key, value, next == null ? null : next.deepCopy());
            }

            public Entry(Object key, Object value, Entry next) {
                this.key = key;
                this.value = value;
                this.next = next;
            }
        }
    }

或者为了防止递归造成的栈溢出,深度拷贝用如下实现:

 Entry deepCopy2() {
                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;
            }

建议

  • clone方法不应该在 构造过程中,调用新对象中任何非final的方法,如果clone调用了一个被覆盖的方法,可能导致克隆对象和原始克隆对象之间的不一致。
  • 覆盖clone方法应该内部处理CloneNotSupportException,而且不会发生类型转换异常,如果类是专门为了继承而设计的,它应该声明clone方法为protected的,并且模拟抛出异常
  • 如果决定用线程安全的类实现Cloneable接口,记得它的clone方法也做好同步

简而言之:所有实现了Cloneable接口的类都应该用一个共有的方法覆盖clone,此方法首先调用super.clone(),然后修正任何需要修正的域。这意味着要拷贝任何包含内部“深层结构”的可变对象。
更多细节可以参考《EffectiveJava》

另外一种实现:拷贝构造器

如果一个对象太过于复杂,可以考虑采用其他方式实现对象拷贝,如提供一个拷贝构造器,它唯一的参数就是包含该构造器的类。如:

public A(A a){
  A copyA = new A();
   ...
   return coayA;
}

拷贝构造器与静态工厂方法的变形,都比Cloneable/clone方具有更多的优势,它们不依赖于很有风险的语言之外的对象创建机制,不需要遵守某些规范….。

更进一步,拷贝构造器或者拷贝工厂可以带有一个参数,参数类型是通过该类实现的接口,由此可以实现转换构造器:

public HashMap newHashMap(Map map){
....
}
public TreeSet newTreeSet(Set set){
...
}

既然Cloneable有这么多问题,可以肯定的说,其他接口都不应该扩展这个接口,为了继承而设计的类也不应该实现这个接口,甚至有很多专家程序员从来不覆盖clone方法,也从来不用,除非是数组拷贝。

你可能感兴趣的:(EffectiveJava——对于所有对象都通用的方法2)