【问题标题】:shallow copy of linkedlist does not reflect changes when adding new node添加新节点时,链表的浅表副本不反映更改
【发布时间】:2019-03-09 18:16:08
【问题描述】:

我已经阅读了很多资料,但如果不在这里提问,我似乎无法消除我的困惑。根据该图,当我使用 clone() 创建链表的浅表副本时。一个新的链表被创建,原始的头变量的引用值被复制到克隆的,其余的节点被共享。因此,如果我使用克隆添加一个新节点,这应该对原始节点可见,不是吗?但是当打印 list1 时,值 3 被省略。谁能告诉我为什么?

LinkedList<Integer> list1 = new LinkedList<>();
l1.add(1);
l1.add(2);
LinkedList<Integer> list2 = (LinkedList) l1.clone();
l2.add(3); 

【问题讨论】:

  • 本题给出的图片可能正确描述了浅拷贝的定义,但没有描述LinkedList.clone()是如何实现的。节点确实是 LinkedList 持有的对象,因此浅拷贝的 LinkedList 持有对原始持有的相同节点的引用是合理的。然而,Node 只是一个辅助类,应该保留在黑盒子中。所以 LinkedList.clone() 也会复制节点。

标签: java linked-list


【解决方案1】:

clone() 创建新的LinkedList 结构并返回对第一个节点的新引用。这两个LinkedLists 之间的关系是它们共享同一个节点values。当您对旧列表或新列表进行一些add\ remove 操作时,这些操作不会更改其他列表。这就是为什么我们这样做copy - 我们不想在更改副本时更改原始链表结构。

来自LinkedList.clone 文档:

返回此LinkedList 的浅表副本。 (元素本身 没有被克隆。) @return 这个LinkedList 实例的浅拷贝

考虑下面的例子:

import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicInteger;

public class LinkedListsApp {

    public static void main(String[] args) throws Exception {
        LinkedList<AtomicInteger> l1 = new LinkedList<>();
        l1.add(new AtomicInteger(100));
        l1.add(new AtomicInteger(200));

        LinkedList<AtomicInteger> l2 = (LinkedList) l1.clone();
        l2.add(new AtomicInteger(300));

        System.out.println(l1);
        System.out.println(l2);

        // change element on first list
        l1.get(0).incrementAndGet();

        System.out.println();
        System.out.println("After change internal state of first element");
        System.out.println(l1);
        System.out.println(l2);
    }
}

上面的代码打印:

[100, 200]
[100, 200, 300]

After change internal state of first element
[101, 200]
[101, 200, 300]

正如我们所见,当我们从第一个列表更改第一个元素的内部状态时,第二个列表也可以看到它。因此,没有每个元素值的深层副本,而是结构的副本 - 节点和顺序的副本。

为了清楚起见,让我们看一下Java 8中的实现:

public Object clone() {
    LinkedList<E> clone = superClone();

    // Put clone into "virgin" state
    clone.first = clone.last = null;
    clone.size = 0;
    clone.modCount = 0;

    // Initialize clone with our elements
    for (Node<E> x = first; x != null; x = x.next)
        clone.add(x.item);

    return clone;
}

看看for-each 循环。它遍历原始列表并将值添加到clone 列表。方法 add 创建新的 Node 对象,该对象存储与原始列表相同的值:x.item

【讨论】:

  • 链表只是一个类,它只有一个字段,头部的 ref 变量。每个节点都有数据和指向下一个节点的引用。你是说链表知道它的所有子节点吗?像linkedlist一样是所有节点的上下文?
  • @PeterNguyen,它包含 2 个参考:firstlast。它是“感知的”,但不仅仅是直接在这些节点上。但是克隆的实现方式是创建新节点并将其添加到新列表中,其内容与旧列表中的相同。如果您对克隆的工作原理有疑问,可以查看实现,它将回答您的疑问。
  • 链表只是一个类,其中一个字段是头节点的引用。每个节点都是一个单独的对象,其中包含对其下一个节点的引用。就像你说的,浅拷贝不会复制节点。所以当我添加一个新节点时,当前最后一个节点的下一个变量是指下一个节点。所以这应该对双方都可见。除非链表是一个公正的容器,否则你所说的对我来说很有意义。
  • @PeterNguyen,我还不够清楚。它不会复制 value 哪个节点“存储”。但它创建了全新的节点。因此,如果我们在 LinkedList 上有 10 个 User 类的实例并且我们克隆它,我们有 10 个新的 Node 指向相同的 10 个 User 类实例。我们需要区分什么是节点,什么是节点存储的值。有时当我们说node 时,我们认为value 由节点存储。在这种情况下,我们需要非常精确才能正确理解它。
  • Ziober 如果是这样的话,现在很清楚了。谢谢老哥!
【解决方案2】:

clone() 方法创建第一个链表的精确副本,然后将其返回到此处的 l2。它所做的是创建一个新实例并从原始实例复制所有字段,然后将其返回给 l2 对象。如果我们想要的只是一个浅拷贝,我们可以简单地使用赋值运算符,在这种情况下,对 l1 所做的任何更改都会反映在 l2 上,因为不会创建新对象,而只会创建引用的副本。

【讨论】:

  • 你错了。赋值运算符不会创建浅拷贝。它只是将链表的引用位置复制到一个新变量。这意味着只有 1 个链表,但在 clone() 方法的情况下,有 2 个。我知道如何实现链表,所以我知道链表只是一个包含根引用的类。它不直接知道除根以外的任何其他节点。
  • 感谢指正……我对赋值运算符的理解有误……
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-11-11
  • 1970-01-01
  • 2018-11-23
  • 1970-01-01
  • 1970-01-01
  • 2010-09-20
  • 1970-01-01
相关资源
最近更新 更多