Effective Java 第三版——13. 谨慎地重写 clone 方法

Tips
《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必不少人都读过,号称Java四大名著之一,不过第二版2009年出版,到如今已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深入的变化。
在这里第一时间翻译成中文版。供你们学习分享之用。java

Effective Java, Third Edition

13. 谨慎地重写 clone 方法

Cloneable接口的目的是做为一个mixin接口(条目 20),公布这样的类容许克隆。不幸的是,它没有达到这个目的。它的主要缺点是缺乏clone方法,而Object的clone方法是受保护的。你不能,不借助反射(条目 65),仅仅由于它实现了Cloneable接口,就调用对象上的 clone 方法。即便是反射调用也可能失败,由于不能保证对象具备可访问的 clone方法。尽管存在许多缺陷,该机制在合理的范围内使用,因此理解它是值得的。这个条目告诉你如何实现一个行为良好的 clone方法,在适当的时候讨论这个方法,并提出替代方案。数组

既然Cloneable接口不包含任何方法,那它用来作什么? 它决定了Object的受保护的clone 方法实现的行为:若是一个类实现了Cloneable接口,那么Object的clone方法将返回该对象的逐个属性(field-by-field)拷贝;不然会抛出CloneNotSupportedException异常。这是一个很是反常的接口使用,而不该该被效仿。 一般状况下,实现一个接口用来表示能够为客户作什么。但对于Cloneable接口,它会修改父类上受保护方法的行为。安全

虽然规范并无说明,但在实践中,实现Cloneable接口的类但愿提供一个正常运行的公共 clone方法。为了实现这一目标,该类及其全部父类必须遵循一个复杂的、不可执行的、稀疏的文档协议。由此产生的机制是脆弱的、危险的和不受语言影响的(extralinguistic):它建立对象而不须要调用构造方法。性能优化

clone方法的通用规范很薄弱的。 如下内容是从 Object 规范中复制出来的:app

建立并返回此对象的副本。 “复制(copy)”的确切含义可能取决于对象的类。 通常意图是,对于任何对象x,表达式x.clone() != x返回 true,而且x.clone().getClass() == x.getClass()也返回 true,但它们不是绝对的要求,但一般状况下,x.clone().equals(x)返回 true,固然这个要求也不是绝对的。ide

根据约定,这个方法返回的对象应该经过调用super.clone方法得到的。 若是一个类和它的全部父类(Object除外)都遵照这个约定,状况就是如此,x.clone().getClass() == x.getClass()性能

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

这种机制与构造方法链(chaining)很类似,只是它没有被强制执行;若是一个类的clone方法返回一个经过调用构造方法得到而不是经过调用super.clone的实例,那么编译器不会抱怨,可是若是一个类的子类调用了super.clone,那么返回的对象包含错误的类,从而阻止子类 clone 方法正常执行。若是一个类重写的 clone 方法是有 final 修饰的,那么这个约定能够被安全地忽略,由于子类不须要担忧。可是,若是一个final类有一个不调用super.clone的clone方法,那么这个类没有理由实现Cloneable接口,由于它不依赖于Object的clone实现的行为。优化

假设你但愿在一个类中实现Cloneable接口,它的父类提供了一个行为良好的 clone方法。首先调用super.clone。 获得的对象将是原始的彻底功能的复制品。 在你的类中声明的任何属性将具备与原始属性相同的值。 若是每一个属性包含原始值或对不可变对象的引用,则返回的对象可能正是你所须要的,在这种状况下,不须要进一步的处理。 例如,对于条目 11中的PhoneNumber类,状况就是这样,可是请注意,不可变类永远不该该提供clone方法,由于这只会浪费复制。 有了这个警告,如下是PhoneNumber类的clone方法:ui

// 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类,可是这个clone方法返回PhoneNumber类。 这样作是合法和可取的,由于Java支持协变返回类型。 换句话说,重写方法的返回类型能够是重写方法的返回类型的子类。 这消除了在客户端转换的须要。 在返回以前,咱们必须将Object的super.clone的结果强制转换为PhoneNumber,但保证强制转换成功。

super.clone的调用包含在一个try-catch块中。 这是由于Object声明了它的clone方法来抛出CloneNotSupportedException异常,这是一个检查时异常。 因为PhoneNumber实现了Cloneable接口,因此咱们知道调用super.clone会成功。 这里引用的须要代表CloneNotSupportedException应该是未被检查的(条目 71)。

若是对象包含引用可变对象的属性,则前面显示的简单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实例相同的数组。 修改原始实例将破坏克隆中的不变量,反之亦然。 你会很快发现你的程序产生了无心义的结果,或者抛出NullPointerException异常。

这种状况永远不会发生,由于调用Stack类中的惟一构造方法。 实际上,clone方法做为另外一种构造方法; 必须确保它不会损坏原始对象,而且能够在克隆上正确创建不变量。 为了使Stack上的clone方法正常工做,它必须复制stack 对象的内部。 最简单的方法是对元素数组递归调用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会返回一个数组,其运行时和编译时类型与被克隆的数组相同。 这是复制数组的首选习语。 事实上,数组是clone 机制的惟一有力的用途。

还要注意,若是elements属性是final的,则之前的解决方案将不起做用,由于克隆将被禁止向该属性分配新的值。 这是一个基本的问题:像序列化同样,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方法分配一个合适大小的新哈希桶数组,迭代原来哈希桶数组,深度复制每一个非空的哈希桶。 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;
}

克隆复杂可变对象的最后一种方法是调用super.clone,将结果对象中的全部属性设置为其初始状态,而后调用更高级别的方法来从新生成原始对象的状态。 以HashTable为例,bucket属性将被初始化为一个新的bucket数组,而且 put(key, value)方法(未示出)被调用用于被克隆的哈希表中的键值映射。 这种方法一般产生一个简单,合理的优雅clone方法,其运行速度不如直接操纵克隆内部的方法快。 虽然这种方法是干净的,但它与整个Cloneable体系结构是对立的,由于它会盲目地重写构成体系结构基础的逐个属性对象复制。

与构造方法同样,clone 方法绝对不能够在构建过程当中,调用一个能够重写的方法(条目 19)。若是 clone 方法调用一个在子类中重写的方法,则在子类有机会在克隆中修复它的状态以前执行该方法,极可能致使克隆和原始对象的损坏。所以,咱们在前面讨论的 put(key, value)方法应该时 final 或 private 修饰的。(若是时 private 修饰,那么大概是一个非 final 公共方法的辅助方法)。

Object 类的 clone方法被声明为抛出CloneNotSupportedException异常,但重写方法时不须要。 公共clone方法应该省略throws子句,由于不抛出检查时异常的方法更容易使用(条目 71)。

在为继承设计一个类时(条目 19),一般有两种选择,但不管选择哪种,都不该该实现 Clonable 接口。你能够选择经过实现正确运行的受保护的 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的线程安全的类,记得它的clone方法必须和其余方法同样(条目 78)须要正确的同步。 Object 类的clone方法是不一样步的,因此即便它的实现是使人满意的,也可能须要编写一个返回super.clone()的同步clone方法。

回顾一下,实现Cloneable的全部类应该重写公共clone方法,而这个方法的返回类型是类自己。 这个方法应该首先调用super.clone,而后修复任何须要修复的属性。 一般,这意味着复制任何包含内部“深层结构”的可变对象,并用指向新对象的引用来代替原来指向这些对象的引用。虽然这些内部拷贝一般能够经过递归调用clone来实现,但这并不老是最好的方法。 若是类只包含基本类型或对不可变对象的引用,那么极可能是没有属性须要修复的状况。 这个规则也有例外。 例如,表示序列号或其余惟一ID的属性即便是基本类型的或不可变的,也须要被修正。

这么复杂是否真的有必要?不多。 若是你继承一个已经实现了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)

考虑到与Cloneable接口相关的全部问题,新的接口不该该继承它,新的可扩展类不该该实现它。 虽然实现Cloneable接口对于final类没有什么危害,但应该将其视为性能优化的角度,仅在极少数状况下才是合理的(条目67)。 一般,复制功能最好由构造方法或工厂提供。 这个规则的一个明显的例外是数组,它最好用 clone方法复制。

相关文章
相关标签/搜索