第11条:谨慎地覆盖clone

第11条:谨慎地覆盖clone

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

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

    如果实现Cloneable接口是要对某个类起到作用,类和它的所有超类都必须遵守一个相当复杂的,不可实施的,并且基本上没有文档说明的协议。由此可以得到一种语言之外的机制:无需调用构造器就可以创建对象。

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

        创建和返回该对象的一个拷贝。这个“拷贝”的精确含义取决于该对象的类。一般的含义是,对于任何对象x,表达式

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

    将会是true,但这些都不是绝对的要求。通常情况下,表达式x.clone.equals(x)将会是true,但是这也不是一个绝对的要求。拷贝对象往往会导致创建它的类的一个新实例,但它同事也会要求拷贝内部的数据结构。这个过程没有调用构造器。

    这个约定存在着几个问题。“不调用构造器”的规定太强硬了。行为良好的clone方法可以调用构造器来创建对象,构造器之后再复制内部数据,如果这个类是final的,clone甚至可能会返回一个由构造器创建的对象。

    然而,x.clone.getClass() == x.getClass()的规定又太软了。在实践中,程序员会假设:如果它们扩展了一个类,并且从子类中调用了super.clone,返回的对象就将是该子类的实例。超类能够提供这种功能的唯一途径是,返回一个通过调用super.clone而获得的对象。如果clone方法返回一个由构造器创建的对象,它就会得到有错误的类。因此,如果覆盖了非final类中的clone方法,则应该返回一个通过调用super.clone而得到的对象。从而创建出正确类的实例。这种机制大体上类似于自动的构造器调用链。

    从1.6发行版本开始,Cloneable接口并没有清楚地指明,一个类在实现这个接口时应该承担哪些责任。实际上,对于实现了Cloneable的类,我们总是期望它也提供一个功能适当的公有的clone方法。通常情况下,除非该类的所有超类都提供了行为良好的clone实现,无论是公有的还是受保护的。

覆盖方法

    假设希望在一个类中实现Cloneable,并且它的超类都提供行为良好的clone方法。从super.clone()中得到的对象可能会接近于最重要返回的对象,也可能相差甚远,这取决于这个类的本质。从每个超类的角度来看,这个对象讲是原始对象功能完整的克隆(clone)。在这个类中声明的域将等同于被克隆的对象中相应的域。如果每个域包含一个基本类型的值,或者包含一个指向不可变对象的引用,那么被返回的对象则可能正式你需要的对象,在这种情况下就不需要再做处理。例如在第9条中的PhoneNumber类正是如此:

public final class PhoneNumber {
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumber(int areaCode, int prefix,
                        int lineNumber) {
        this.areaCode = (short) areaCode;
        this.prefix = (short) prefix;
        this.lineNumber = (short) lineNumber;
    }
}

    在这种情况下,需要做的事情,除了声明实现了Cloneable之外,就是对Object中受保护的clone方法提供公有的访问途径:

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

    注意上述的clone方法返回的是PhoneNumber,而不是返回的Object。从Java1.5发行版本开始,这么做就是合法的,因为1.5发行版本引入了协变返回类型(convariant return type)作为泛型。目前覆盖方法的返回类型可以是被覆盖方法的返回类型的子类,这样有助于覆盖方法返回更多关于被返回对象的信息,并且在客户端中不必进行类型转换。由于Object.clone返回Object,PhoneNumber.clone必须在返回super.clone()的结果之前将其转换。这里体现了一条通则:永远不要让客户去做任何类库能够替客户完成的事情

    如果对象中包含的域引用了可变的对象,使用上述这种简单的clone实现可能会导致灾难性的后果= =,例如下面这个例子:

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    publci Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

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

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

    /**
     * Ensure space for at least one more element
     * roughly doubling the capacity each time the 
     * array needs to grow
     * /
    private void ensureCapacity() {
        if (size == elements.length)
            elements = Arrays.copyOf(elements, 2* size + 1);
    }
}

    假设希望把这个类做成可克隆的。如果它的clone方法仅仅返回super.clone(),这样得到的Stack实例,在其size域中具有正确的值,但是它的elements域将引用与原Stack实例相同的数组。修改原始的实例则会破坏克隆出来对象中的约束条件。很快就会发现,这个克隆出来的对象根本没有什么用,和之前的那个对象公用着一段数据。

    如果调用Stack类中唯一的构造器,这种情况就永远不会发生。实际上,clone放阿飞就是另外的一个构造器:你必须确保它不会伤害到原始的对象,并且确保正确的创建被克隆对象中的约束条件(invariant)。为了使Stack类中的clone方法正常的工作,它必须要拷贝栈的内部信息。最容易的做法是,在elements数组中递归地调用clone:

@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[]。从Java1.5发行版本开始,在数组上调用clone返回的数组,其编译时类型与被克隆数组的类型相同。还需要注意的是,如果elements域是final的,上述的方法就不能正常的工作了,因为clone方法是被禁止给elements域赋新值的。这是个根本的问题:clone架构与引用可变对象的final域的正常用法是不兼容的,除非在原始对象和克隆对象爱那个之前可以安全的共享此可变对象。为了使类成为可克隆的,可能有必要从某些域中去掉final修饰符。

    递归调用clone有时还不够。例如,假设正在为一个散列表编写clone方法,它的内部数据包含一个散列桶数组,每个散列桶都指向”key-value”链表的第一个项。也就是说,该数据结构是数组元素为链表的数组,每个链表的节点具有两个数据类型。该类的代码如下:

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;
        }
    }
}

    假设我们像下面这样做:

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

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

public class HashTable impement Cloneable {
    private Enrty[] 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;
    }

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

@Override
public HashTable clone() {
    try {
        HashTable result = (HashTable) super.clone();
        result.bucket = new Entry[bucket.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();
        }
    }
}

    私有类HashTable.Entry被加强了,它支持了一个deepCopy()方法。HashTable上的clone方法分配了一个大小适中的,新的buckets数组,对每个非空的链表进行deepCopy。Entry类中的深度拷贝方法递归地调用了本身,以便拷贝整个链表。虽然这种方法很灵活,但是当链表长度很大的时候,不停的递归入栈,很容易就导致了栈的溢出。为了避免这种情况,可以用迭代来替代递归:

Entry depCopy() {
    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(),然后把结果对象中的所有域都设置成空白的状态,然后调用高层方法来重新产生对象的状态。例如上面的这个例子中,buckets域将被初始化为一个新的散列桶数组,然后,对于正在被克隆的散列表中的每一个key-value映射,都调用put(key, value)方法。这种做法往往会产生一个简单,合理的clone方法,但是它运行起来通常没有”直接操作对象及克隆对象的内部状态的clone方法”快速。

    Object的clone方法被声明为可抛出CloneNotSupportedException异常,但是覆盖版本的clone方法可能也会忽略这个声明。如果专门为了设计而继承的类覆盖了clone方法,覆盖版本的clone方法就应该模拟Object.clone的行为:它应该被声明为protected,抛出CloneNotSupportedException异常,并且该类不应该实现Cloneable接口。这样做可以使子类具有实现或者不实现Clonable接口的自由,就仿佛它们直接扩展了Object一样。

    简而言之,所有实现了Cloneable接口的类都应该用一个公有的方法覆盖clone。该公有方法首先调用super.clone,然后修正任何需要修正的域。一般情况下,这意味着要拷贝任何包含内部“深层结构”的可变对象(例如例子中的Entry),并用指向新对象的引用替代原来指向这些对象的引用。虽然,这些内部拷贝操作往往可以通过递归地调用clone来完成,但是这通常不是最佳的方法。如果你扩展了一个实现了Cloneable接口的类,那么除了实现一个行为良好的clone方法之外,没有别的选择。

替代方法

    另一个实现对象拷贝的好办法是提供一个拷贝构造器(copy constructor)或拷贝工厂(copy factory)。拷贝构造器只是一个构造器,它唯一的参数类型是包含该构造器的类,例如:

public Yum(Yum yum);

    拷贝工厂是类细语拷贝构造器的静态工厂:

public static Yum newInstance(Yun yum);

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

    更进一步,拷贝构造器或者拷贝工厂可以带一个参数,参数类型是通过该类实现的接口。例如,按照管理,所有通用集合实现都提供了一个拷贝构造器,它的参数类型为Collection或者是Map。基于接口的拷贝构造器和拷贝工厂,能允许用户选择拷贝的实现类型,而不是强迫客户接受原始的实现类型。例如,现在有一个HashSet,并且希望把它拷贝成一个TreeSet。clone方法无法提供这样的功能,但是用拷贝构造器很容易实现,new TreeSet(s)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值