为什么子列表的更改会在原始列表中反映出来?

11

我知道在Java中,如果你通过引用传递 Collections 列表对象,它们是可变的。
我想确切地知道原始列表和其子列表在内存地址中发生了什么。
子列表和原始列表是否引用同一个对象?

以下是反映子列表对主原始列表所做更改的代码示例。

List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add(1, "3");

List<String> list2 = new LinkedList<String>(list);

list.addAll(list2);

list2 = list.subList(2, 5);
list2.clear();               //Changes are made to list

System.out.println(list);
4个回答

12
根据有关此事的JavaDoc
List subList(int fromIndex, int toIndex)
返回列表中指定fromIndex(包括)和toIndex(不包括)之间部分的视图。(如果fromIndex和toIndex相等,则返回的列表为空。)返回的列表由此列表支持,因此在返回的列表中进行的非结构性更改会反映在此列表中,反之亦然。返回的列表支持此列表支持的所有可选列表操作。
子列表将指向原始列表中存在的相同元素,因此,通过子列表进行的任何更改都将反映在原始列表中,因为您正在更改相同的对象。
编辑:根据您的评论,假设原始列表具有以下引用:0x00 0x01 0x02 0x03 0x04 0x05,并且这些引用到内存中存在的对象的位置。
对上述内容执行sublist(0, 2)将生成一个列表,其中包含指向以下内存位置0x00 0x01 0x02的指针,这与原始列表中的相同。这意味着,如果你执行sublist.get(0).setFoo(foo),它会定位到0x00处的对象并设置某些属性。然而,0x00也被original list引用,这就是为什么改变子列表意味着你将会改变源列表因为两个列表指向同一对象。如果通过original list更改元素,情况也一样。

你的意思是关于内存中发生了什么?就像任何引用相同对象的事情一样。这里没有什么特别的。 - Brandon Ling
@ShubhamKharde:我已经尝试扩展我的答案。如果你仍然有问题,你需要了解一般引用是如何工作的。 - npinti
@Brandon Ling,这些更改会如何反映?参考文献、子字符串和主列表都必须指向同一个对象吗?还是子列表对象包含类似于“指针”的东西来存储相应对象值的引用位置? - Shubham Kharde
小问题,为什么在 sublist(0, 2) 中的列表中有4个元素? - phlaxyr
@phlaxyr:那个错别字拖了很久,现在已经修复了。 - npinti
sublist(0, 2)有两个元素0和1。根据文档,索引为2的元素被排除在外。那么为什么你有3个引用(0x00 0x01 0x02)? - user2112247

4

在线

list2 = list.subList(2, 5);

你正在调用 list 引用的 ArrayListsubList 方法。它的代码如下:
public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
}

所以在确认有效范围后,list2将存储结果。
new SubList(this, 0, fromIndex, toIndex);

在这里private class SubList extends AbstractList<E>是定义在ArrayList内部的类,这个构造函数的代码如下

SubList(AbstractList<E> parent,
        int offset, int fromIndex, int toIndex) {
    this.parent = parent;
    this.parentOffset = fromIndex;
    this.offset = offset + fromIndex;
    this.size = toIndex - fromIndex;
    this.modCount = ArrayList.this.modCount;
}

因此,它的parent字段将存储对原始ArrayList的引用(new SubList(this, ...))。

现在,当您调用

list2.clear();

clear()方法的代码将从AbstractList继承给SubList,并被调用。

public void clear() {
    removeRange(0, size());
}

这将在内部调用SubList中重写的removeRange方法。

protected void removeRange(int fromIndex, int toIndex) {
    checkForComodification();
    parent.removeRange(parentOffset + fromIndex,
                       parentOffset + toIndex);
    this.modCount = parent.modCount;
    this.size -= toIndex - fromIndex;
}

因此,正如您所看到的,您调用的结果是

parent.removeRange(parentOffset + fromIndex,
                   parentOffset + toIndex);

回忆一下,parent 持有对调用 subList 的 ArrayList 的引用。因此,通过调用 clear,你实际上是从创建子列表的原始列表中调用 removeRange


1
谢谢。人们只是不想深入探讨,如果他们的答案不能被接受,他们就会对问题进行投票贬值。对于那些对问题进行了投票的人,这就是我所期望的答案,他们必须遵守问题中所要求的内容。 - Shubham Kharde
1
@ShubhamKharde 人们通常不会因为他们的答案没有被接受而进行负评,而是因为他们认为问题不清楚或者缺乏证明或研究。说实话,我也不确定那是否是你要找的答案,但是既然我决定了需要刷新一下我的集合知识,我就做了一些研究并将它们发布为答案。 - Pshemo
@ShubhamKharde 看起来很相似,他是用内存地址描述代码,我展示的是调用哪些方法、何时以及在哪些引用上调用它们,以向您展示主要观点,即最终调用 list2.clear() 将导致从原始 list 中调用 removeRange - Pshemo
1
重点是sibList不会创建一个列表,其中复制了原始列表中所有对象的引用,而只是记住了应该表示的列表中第一个和最后一个元素的索引(范围)。因此,在子列表上调用方法与在原始列表的某个范围上调用这些方法是相同的。 - Pshemo

3
以下是基于java源代码的内存示例的简化可视化图,作为对Pshemo优秀答案的补充:
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add(1, "3");

List<String> list2 = new LinkedList<String>(list);

list.addAll(list2);

enter image description here


list2 = list.subList(2, 5);

SubList有一个对原始列表的引用,其中包含一个偏移量来确定子列表的起始位置和一个大小来确定子列表的结束位置。

enter image description here


list2.clear();

列表元素的操作将转发到原始列表。

enter image description here

请注意,通过将引用从索引5复制到索引2并使用null填充数组的索引3、4和5,这些元素被移除。

2

请查看链接

SubList返回此列表中指定的fromIndex(包括)和toIndex(不包括)之间的部分视图。 (如果fromIndex和toIndex相等,则返回的列表为空。)返回的列表由此列表支持,因此返回列表中的非结构性更改会反映在此列表中,反之亦然。 返回的列表支持此列表支持的所有可选列表操作。

所以你的list2只是原始列表的子视图。这就是为什么当你清除list2时,你会失去原始列表中对应的值。请参考以下代码。

public static void main(String[] args)
    {   
        List<String> list = new ArrayList<String>();
        list.add("1");
        list.add("2");
        list.add(1, "3");
        List<String> list2 = new LinkedList<String>(list);
        list.addAll(list2);
        System.out.println(list);
        list2 = list.subList(2, 5);
        System.out.println(list2);
        list2.clear();               //Changes are made to list1
        System.out.println(list);

    }

输出

[1, 3, 2, 1, 3, 2]
[2, 1, 3]
[1, 3, 2]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接