​【第13条】谨慎地覆盖clone

谨慎地覆盖clone

Cloneable接口的目的是作为对象的一个mixin接口(mixin interface)(详见第20条),表明这样的对象允许克隆(clone)。遗憾的是,它并没有成功地达到这个目的。它的主要缺陷在于缺少一个clone方法,而Object的clone方法是受保护的。如果不借助于反射(reflection)(详见第65条),就不能仅仅因为一个对象实现了Cloneable,就调用clone方法。即使是反射调用也可能会失败,因为不能保证该对象一定具有可访问的clone方法。尽管存在这样或那样的缺陷,这项设施仍然被广泛使用,因此值得我们进一步了解。本条将告诉你如何实现一个行为良好的clone方法,并讨论何时适合这样做,同时也简单地讨论了其他的可替代做法。

既然Cloneable接口并没有包含任何方法,那么它到底有什么作用呢?它决定了Object中受保护的clone方法实现的行为:如果一个类实现了Cloneable,Object的clone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException异常。这是接口的一种极端非典型的用法,也不值得仿效。通常情况下,实现接口是为了表明类可以为它的客户做些什么。然而,对于Cloneable接口,它改变了超类中受保护的方法的行为。

虽然规范中没有明确指出,事实上,实现CloneabIe接口的类是为了提供一个功能适当的公有的cione方法。为了达到这个目的,类及其所有超类都必须遵守一个相当复杂的、不可实施的,并且基本上没有文档说明的协议。由此得到一种语言之外的(extralinguistic)机制:它无须调用构造器就可以创建对象。

clone方法的通用约定是非常弱的,下面是来自Object规范中的约定内容:

创建和返回该对象的一个拷贝。这个“拷贝”的精确含义取决于该对象的类。一般的含义是,对于任何对象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返回对象之前,修改对象的一个或更多个域。

这种机制大体上类似于自动的构造器调用链,只不过它不是强制要求的:如果类的clone方法返回的实例不是通过调用super.clone方法获得,而是通过调用构造器获得,编译器就不会发出警告,但是该类的子类调用了super.clone方法,得到的对象就会拥有错误的类,并阻止了clone方法的子类正常工作。如果final类覆盖了clone方法,那么这个约定可以被安全地忽略,因为没有子类需要担心它。如果final类的clone方法没有调 用super.clone方法,这个类就没有理由去实现Cloneable接口了,因为它不依赖于Object克隆实现的行为。

假设你希望在一个类中实现Cloneable接口,并且它的超类都提供了行为良好的clone方法。首先,调用super.clone方法。由此得到的对象将是原始对象功能完整的克隆(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,但这个clone方法返回的却是PhoneNumber。这么做是合法的,也是我们所期望的,因为Java支持协变返回类型(covariant return type)。换句话说,目前覆盖方法的返回类型可以是被覆盖方法的返回类型的子类了。这样在客户端中就不必进行转换了。我们必须在返回结果之前,先将super .clone从Object转换成 PhoneNumber,当然这种转换是一定会成功的。

对super.clone方法的调用应当包含在一个try-catch块中。这是因为Object声明其clone方法抛出CloneNotSupportedException,这是一个受检异常(checked exception)o 由于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);
   }
}

假设你希望把这个类做成可克隆的(cloneable)。如果它的clone方法仅仅返回super.clone(),这样得到的Stack实例,在其size域中具有正确的值,但是它的elements 域将引用与原始Stack实例相同的数组。修改原始的实例会破坏被克隆对象中的约束条件,反之亦然。很快你就会发现,这个程序将产生毫无意义的结果,或者抛出NullPointerException异常。

如果调用Stack类中唯一的构造器,这种情况就永远不会发生。实际上,clone方法就是另一个构造器;必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件(invariant).为了使Stack类中的clone方法正常工作,它必须要拷贝栈的内部信息。最容易的做法是,在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返回的数组,其编译时的类型与被克隆数组的类型相同。这是复制数组的最佳习惯做法。事实上,数组是clone方法唯一吸引人的用法。

还要注意如果elements域是final的,上述方案就不能正常工作,因为clone方法是被禁止给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被加强了,它支持一个"深度拷贝”(deep copy)方法。HashTable上的clone方法分配了一个大小适中的、新的buckets数组,并且遍历原始的buckets数 组,对每一个非空散列桶进行深度拷贝。Entry类中的深度拷贝方法递归地调用它自身,以便拷贝整个链表(它是链表的头节点)。虽然这种方法很灵活,如果散列桶不是很长,也会工作得很好,但是,这样克隆一个链表并不是一种好办法,因为针对列表中的每个元素,它都要消耗一段栈空间。如果链表比较长,这很容易导致栈溢出。为了避免发生这种情况,你可以在 deepCopy 方法中用迭代(iteration)代替递归(recursion):

// 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方法,然后把结果对象中的所有域都设置成它们的初始状态(initial state),然后调用高层(higher-level)的方法来重新产生对象的状态。在我们的HashTable例子中,buckets域将被初始化为一个新的散列桶数组,然后,对于正在被克隆的散列表中的每一个键-值映射,都调用put(key, value)方法(上面没有给出其代码)。这种做法往往会产生一个简单、合理且相当优美的clone方法,但是它运行起来通常没有“直接操作对象及其克隆对象的内部状态的clone方法”快。虽然这种方法干脆利落,但它与整个Cloneable架构是对立的,因为它完全抛弃了 Cloneable架构基础的逐域对象复制的机制。

像构造器一样,clone方法也不应该在构造的过程中,调用可以覆盖的方法(详见第19条)。如果clone调用了一个在子类中被覆盖的方法,那么在该方法所在的子类有机会修正它在克隆对象中的状态之前,该方法就会先被执行,这样很有可能会导致克隆对象和原始对象之间的不一致。因此,上一段中讨论到的put(key, value)方法要么应是final的,要么应是私有的。(如果是私有的,它应该算是非final公有方法的“辅助方法”。)

Object的clone方法被声明为可抛出CloneNotSupportedException异常,但是,覆盖版本的clone方法可以忽略这个声明。公有的clone方法应该省略throws声明,因为不会抛出受检异常的方法使用起来更加轻松(详见第71条)。

为继承(详见第19条)设计类有两种选择,但是无论选择其中的哪一种方法,这个类都不应该实现Cloneable接口。你可以选择模拟Object的行为:实现一个功能适当的受保护的clone方法,它应该被声明抛出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方法没有同步,即使很满意可能也必须编写同步的clone方法来调用super.clone() , synchronized clone()方法。

简而言之,所有实现了 Cloneable接口的类都应该覆盖clone方法,并且是公有的方法,它的返回类型为类本身。该方法应该先调用super.clone方法,然后修正任何需 要修正的域。一般情况下,这意味着要拷贝任何包含内部“深层结构”的可变对象,并用指向新对象的引用代替原来指向这些对象的引用。虽然,这些内部拷贝操作往往可以通过递归地调用clone来完成,但这通常并不是最佳方法。如果该类只包含基本类型的域,或者指向不可变对象的引用,那么多半的情况是没有域需要修正。这条规则也有例外。例如,代表序列号或其他唯一ID值的域,不管这些域是基本类型还是不可变的,它们也都需要被修正。

真的有必要这么复杂吗?很少有这种必要。如果你扩展一个实现了 Cloneable接口的类,那么你除了实现一个行为良好的clone方法外,没有别的选择。否则,最好提供某些其他的途径来代替对象拷贝。对象拷贝的更好的办法是提供一个拷贝构造器(copy constructor)或拷贝工厂(copy factory)。拷贝构造器只是一个构造器,它唯一的参数类型是包含该构造器的类,例如:

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

拷贝工厂是类似于拷贝构造器的静态工厂(详见第1条):

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

拷贝构造器的做法,及其静态工厂方法的变形,都比Cloneable/clone方法具有更多的优势:它们不依赖于某一种很有风险的、语言之外的对象创建机制;它们不要求遵守尚未制定好文档的规范;它们不会与final域的正常使用发生冲突;它们不会抛岀不必要的受检异常;它们不需要进行类型转换。

甚至,拷贝构造器或者拷贝工厂可以带一个参数,参数类型是该类所实现的接口。例如,按照惯例所有通用集合实现都提供了一个拷贝构造器,其参数类型为Collection或者Map接口。基于接口的拷贝构造器和拷贝工厂(更准确的叫法应该是转换构造器(conversion constructor)和转换工厂(conversion factory)),允许客户选择拷贝的实现类型,而不是强迫客户接受原始的实现类型。例如,假设你有一个HashSet:s,并且希望把它拷贝成一个TreeSet。 clone方法无法提供这样的功能,但是用转换构造器很容易实现:new TreeSet<>(s)。

既然所有的问题都与Cloneable接口有关,新的接口就不应该扩展这个接口,新的可扩展的类也不应该实现这个接口。虽然final类实现Cloneable接口没有太大的危害,这个应该被视同性能优化,留到少数必要的情况下才使用(详见第67条)。总之,复制功能最好由构造器或者工厂提供。这条规则最绝对的例外是数组,最好利用clone方法复制数组。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值