有效的Java声称elements.clone()足够

时间:2018-07-25 09:56:35

标签: java

我正在阅读Joshua Bloch的 Effective Java,第二版,第11项:明智地覆盖克隆。

在第56页,他试图解释说,当我们为某些类(例如集合类)覆盖clone()时,我们必须复制其内部。然后,他给出了设计类Stack的示例:

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    public Stack() {...}
    public void push(Object e) {...}
    public Object pop() {...}
    private void ensureCapacity() {...} //omitted for simplicity
}

他声称,如果我们仅使用super.clone()来克隆Stack,则生成的Stack实例“在其size字段中将具有正确的值,但其element字段将指向与原始的Stack实例。修改原始的实例将破坏克隆中的不变性,反之亦然。您将很快发现您的程序产生了荒谬的结果或抛出了NullPointerException。” 现在,这似乎很公平。但是他随后给出了“正确实现”的示例,这使我感到困惑:

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

现在与super.clone()有何不同?我知道,新的Stack.element将与旧的和所有的引用不同。但是数组的“内部”还是一样的,不是吗?数组result.element的实际元素仍指向原始Object引用。更改原始副本时,这仍然可能导致破坏克隆的不变性,反之亦然,不是吗?我有什么想念的吗?

4 个答案:

答案 0 :(得分:5)

  

现在与super.clone()有什么不同?

因为数组现在不同了。如果两个Stack共享同一数组,则当一个添加或从堆栈中删除时,另一个size中的Stack字段不会更新,从而导致差异。

数组的对象本身不会被克隆。这是有意的,因为不需要克隆它们。预期两个Stack-或实际上两个Collection-可以包含对相同对象的引用。使用此代码,您将得到相同的行为:

Foo foo = new Foo()
Stack stackOne = new Stack();
Stack stackTwo = new Stack();
stackOne.push(foo);
stackTwo.push(foo);

这不是天生的问题,通常是理想的行为。

答案 1 :(得分:2)

您对clone的工作原理绝对正确。支持数组中的对象不会被复制,但是支持数组中的对象将被复制。

这不是问题,因为调用者不希望元素被复制。对于像堆栈这样的集合类,“规范”是进行浅表复制。标准库中的一个示例是ArrayList的副本构造函数。

还请注意,您可以通过克隆数组内部的对象来实现clone(这意味着堆栈只能存储Clonable个暴露{{ 1}})。这不会破坏clone的合同。 contract非常宽松。

答案 2 :(得分:1)

  

修改原始图将破坏克隆中的不变式,反之亦然。

问题是,修改一个堆栈(将新元素推入其中或将其删除)会修改两个堆栈(如果它们要共享同一后备数组,但不一致),例如size成员将在其中一个而不是另一个中更新。例如,如果不变量之一是数组中当前“堆栈顶部”之后的元素都不为空,则该不变量可能会被破坏。 (但是,对于这种特殊情况,我认为这不会直接导致异常)。

通过克隆,两个堆栈具有独立的数组(包含相同的元素),正如您推测的那样。然后将一个元素压入一个堆栈不会影响支持另一堆栈的数组的内容。

  

数组result.element的实际元素仍然指向原始Object引用。更改原始副本时,这仍然可能导致破坏克隆的不变性,反之亦然,不是吗?我想念什么吗?

不,不能。 (尝试提供一个如何发生这种情况的示例;您会发现自己很困惑)。堆栈类的不变量取决于数组的大小和内容(标识),而不取决于数组中对象的状态。

答案 3 :(得分:1)

您无能为力。 super.clone()复制值成员,elements.clone()创建一个新数组,因此新堆栈将具有与旧堆栈无关的独立存储,也就是堆栈结构本身的全部存储。
堆栈上的对象不一定是可克隆的,因此尝试克隆还是不逐个克隆是一个决定性的问题,这很可能是在文本中的某个地方解决的。 (附带说明:当您addAll()putAll()或使用接受另一个容器的构造函数时,内置容器不会克隆对象)