【问题标题】:Effective Java claims that elements.clone() suffices有效的 Java 声称 elements.clone() 就足够了
【发布时间】:2019-01-02 02:27:13
【问题描述】:

我正在阅读 Joshua Bloch 的 Effective Java,第 2 版,第 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 字段中具有正确的值,但它的 elements 字段将引用与原始 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 引用。这仍然可能导致在更改原始文件时破坏克隆的不变量,反之亦然,不是吗?我错过了什么吗?

【问题讨论】:

  • 你可以找到here,你说的是对的。 elements.clone() 不会克隆内容。

标签: java


【解决方案1】:

修改原始版本会破坏克隆版本中的不变量,反之亦然。

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

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

数组 result.element 的实际元素仍然指向原始 Object 引用。这仍然可能导致在更改原始文件时破坏克隆的不变量,反之亦然,不是吗?我错过了什么吗?

不,它不能。 (试着想出一个例子来说明这种情况是如何发生的;你会发现自己被难住了)。堆栈类的不变量取决于数组大小和内容(标识),而不是数组中对象的状态。

【讨论】:

    【解决方案2】:

    现在这与 super.clone() 有何不同?

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

    数组的对象本身不会被克隆。这是故意的,因为它们不需要被克隆。预计两个Stacks - 或者实际上任何两个Collections - 可以包含对相同对象的引用。使用此代码,您将获得相同的行为:

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

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

    【讨论】:

    • 谢谢。我不明白的是,当两个数组都包含指向同一组对象的引用时,其中一个删除了一个元素,另一个数组(其size 没有改变)仍将保留对该 deleted 元素,因此它的不变量仍然存在。
    【解决方案3】:

    您对clone 的工作方式是完全正确的。不会复制后备数组中的对象,但会复制后备数组。

    这不是问题,因为调用者并不期望元素被复制。对于像栈这样的集合类,“规范”是做一个浅拷贝。标准库中的一个示例是 ArrayList 的复制构造函数。

    还请注意,您可以通过克隆数组中的对象来实现clone(这意味着堆栈只能存储暴露cloneClonable 对象)。这不会违反clone 的合同。 contract 很松散。

    【讨论】:

    • 如果您确实想克隆每个对象,变量类型必须是实现Clonable 并公开clone() 方法的类型。您不能在 Object 引用上调用 clone()
    • 顺便说一句,“实现 Cloneable”和“公开 clone() 方法”是两个独立的要求。您也不能在 Cloneable 类型的引用上调用 clone()
    • 关于ArrayList 的复制构造函数的要点。并感谢您提出克隆集合的“规范”方式的想法。
    【解决方案4】:

    除此之外,您无能为力。 super.clone() 复制值成员,elements.clone() 创建一个新数组,因此新堆栈将拥有与旧堆栈独立的存储空间,仅此而已,用于堆栈构造本身。
    堆栈上的对象不一定是可克隆的,因此需要一个一个尝试克隆或不克隆它们的决定,这很可能在文本中的某处得到解决。 (旁注:当您addAll()putAll() 或使用接受另一个容器的构造函数时,内置容器不会克隆对象)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-03-08
      • 2010-09-23
      • 1970-01-01
      • 1970-01-01
      • 2011-01-05
      • 2017-10-05
      • 2023-04-11
      • 2023-01-24
      相关资源
      最近更新 更多