Effective Java 第三版读书笔记——条款13:审慎而明智地重写 clone 方法

Cloneable 接口不包含任何方法,它只是决定了 Object 中受保护的 clone 方法的行为:如果一个类实现了 Cloneable 接口,那么 Object 的 clone 方法将返回该对象的逐个属性(field-by-field)拷贝;否则会抛出 CloneNotSupportedException 异常。这是一个非常反常的接口使用,不应该被效仿。通常情况下,实现一个接口用来表示这个类可以为客户做什么。

clone 方法的通用规范很弱,以下内容是从 Object 注释中复制出来的:

创建并返回此对象的副本。 “复制(copy)”的确切含义可能取决于对象的类。一般意图是,对于任何对象 x,表达式

x.clone() != x

返回 true,并且

x.clone().getClass() == x.getClass()

也返回 true,但它们不是绝对的要求。通常情况下,

x.clone().equals(x)

返回 true,当然这个要求也不是绝对的。

根据约定,这个方法返回的对象应该通过调用 super.clone 方法获得。如果一个类和它的所有父类(Object 除外)都遵守这个约定,情况就是如此:

x.clone().getClass() == x.getClass()

根据约定,返回的对象应该独立于被克隆的对象。为了实现这种独立性,在返回对象之前,可能需要修改由 super.clone 返回的对象的一个或多个属性。

假设你希望在一个类中实现 Cloneable 接口,它的父类提供了一个行为良好的 clone 方法。首先调用 super.clone。得到的对象将是原始对象在功能上的完全复制品。在你的类中声明的任何属性将具有与原始属性相同的值。如果每个属性都是基本类型或对不可变对象的引用,则返回的对象可能正是你所需要的,在这种情况下不需要进一步的处理。例如,对于条款 11 中的 PhoneNumber 类就是这样。但是请注意,不可变类永远不应该提供 clone 方法,因为这只会浪费复制。下面是 PhoneNumber 类的 clone 方法:

// Clone method for class with no references to mutable state
@Override public PhoneNumber clone() {
    try {
        return (PhoneNumber) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();  // Can't happen
    }
}

为了使这个方法起作用,PhoneNumber 的类声明必须被修改,以表明它实现了 Cloneable 接口。虽然 Object 类的 clone 方法返回 Object 类,但是 PhoneNumber 类的 clone 方法返回 PhoneNumber 类。这样做是合法和可取的,因为Java支持协变返回类型。换句话说,重写方法的返回类型可以是被重写方法的返回类型的子类。

如果对象属性包含对可变对象的引用,则前面显示的简单的 clone 实现可能是灾难性的。例如,考虑条款 7 中的 Stack 类:

public class Stack {

    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];

        elements[size] = null; // Eliminate obsolete reference
        return result;
    }

    // Ensure space for at least one more element.
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

假设你想让这个类可以克隆。如果 clone 方法仅返回 super.clone() 返回的对象,那么生成的 Stack 实例在其 size 属性中具有正确的值,但 elements 属性将与原始 Stack 实例中的elements 属性引用相同的数组。修改原始实例将同时克隆实例中的值,反之亦然。

实际上,clone 方法作为另一种构造方法,必须确保它不会损坏原始对象,并且可以在克隆上正确建立不变量。为了使 Stack 上的 clone 方法正常工作,它必须复制 stack 对象的内部属性。最简单的方法是对 elements 数组递归调用 clone 方法:

// Clone method for class with references to mutable state
@Override public Stack clone() {
    try {
        Stack result = (Stack) super.clone();
        result.elements = elements.clone();
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

请注意,我们不必将 elements.clone 的结果转换为 Object[] 数组。在数组上调用 clone 会返回一个数组,其运行时和编译时类型与被克隆的数组相同。还要注意,如果 elements 属性是 final 的,则以前的解决方案将不起作用,因为 clone 将被禁止向该属性赋新的值 —— Cloneable 体系结构与引用可变对象的 final 属性的正常使用不兼容。为了使一个类可以克隆,可能需要从一些属性中移除 final 修饰符。

仅仅递归地调用 clone 方法并不总是足够的。例如,假设你正在为哈希表编写一个 clone 方法,其内部包含一个哈希桶数组,每个哈希桶都指向“键-值”对链表的第一项。为了提高性能,该类实现了自己的轻量级单链表,而没有使用 java 内部提供的 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;  
        }
    }
    ... // Remainder omitted
}

假设你只是递归地克隆哈希桶数组,就像我们为 Stack 所做的那样:

// Broken clone method - results in shared mutable state!
@Override public HashTable clone() {
    try {
        HashTable result = (HashTable) super.clone();
        result.buckets = buckets.clone();
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

虽然被克隆的对象有自己的哈希桶数组,但是这个数组与原始数组引用了相同的链表,这很容易导致克隆对象和原始对象中的不确定性行为。要解决这个问题,你必须复制包含每个桶的链表。下面是一种常见的方法:

// Recursive clone method for class with complex mutable state
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
}

私有类 HashTable.Entry 已被扩充以支持“深度复制”方法。HashTable 上的 clone 方法分配一个合适大小的新 buckets 数组,并且迭代原来的 buckets 数组,深度复制每个非空的哈希桶。Entry 上的 deepCopy 方法递归地调用它自己以复制由头节点开始的整个链表。如果哈希桶不是太长,这种技术很聪明并且可以正常工作。但是,克隆链表不是一个好方法,因为它为列表中的每个元素消耗一个栈帧(stack frame)。如果列表很长,这很容易导致堆栈溢出。为了防止这种情况发生,可以用循环迭代来替换 deepCopy 中的递归:

// Iteratively copy the linked list headed by this 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;
}

在为继承设计一个类时(条款 19),通常有两种选择,但无论选择哪一种,都不应该实现 Clonable 接口。你可以选择通过实现正确运行的 protected 修饰的 clone 方法来模仿 Object 的行为,该方法声明为抛出 CloneNotSupportedException 异常。这给了子类实现 Cloneable 接口的自由,就像直接继承 Object 一样。或者,可以选择不去实现工作的 clone 方法,并通过提供以下 clone 实现来阻止子类实现它:

// clone method for extendable class not supporting Cloneable
@Override
protected final Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
}

回顾一下,实现 Cloneable 的所有类应该重写 public 修饰的 clone 方法,而这个方法的返回类型是类本身。这个方法应该首先调用 super.clone,然后修复任何需要修复的属性。通常,这意味着复制任何包含内部“深层结构”的可变对象,并用指向新对象的引用来代替原来指向这些对象的引用。虽然这些内部拷贝通常可以通过递归调用 clone 来实现,但这并不总是最好的方法。如果类只包含基本类型或对不可变对象的引用,那么很可能没有属性需要修复。这个规则也有例外。例如,表示序列号或其他唯一 ID 的属性即使是基本类型的或不可变的,也需要被修正。

然而,这么复杂的 clone 方法很少有必要。如果你继承一个已经实现了 Cloneable 接口的类,你别无选择,只能实现一个行为良好的 clone 方法。否则,你最好提供另一种对象复制方法。对象复制的更好方法是提供一个复制构造方法或复制工厂

复制构造方法是只接受一个参数的构造方法,这个参数的类型为包含此构造方法的类,例如:

// Copy constructor
public Yum(Yum yum) { ... };

复制工厂是类似于复制构造方法的静态工厂:

// Copy factory
public static Yum newInstance(Yum yum) { ... };

复制构造方法及其静态工厂变体与 Cloneable/clone 相比有许多优点:它们不依赖风险很大的语法之外的对象创建机制;不要求遵守那些不太明确的惯例;不会与 final 属性的正确使用相冲突;不会抛出不必要的检查异常;而且不需要类型转换。

此外,复制构造方法或复制工厂可以接受类型为该类实现的接口的参数。例如,按照惯例,所有通用集合实现都提供了一个构造方法,其参数的类型为 Collection 或 Map。基于接口的复制构造方法和复制工厂(更适当地称为转换构造方法和转换工厂)允许客户端选择复制的实现类型,而不是强制客户端接受原始实现类型。例如,假设你有一个 HashSet,并且你想把它复制为一个 TreeSet。clone 方法不能提供这种功能,但使用转换构造方法很容易:new TreeSet<>(s)

猜你喜欢

转载自blog.csdn.net/sky_asd/article/details/84702428