为什么一个 ArrayList 上的 clear() 会清除另一个 ArrayList 中的元素?

Why clear() on one ArrayList is clearing elements in another ArrayList?

我想要的输出应该是:
[[1], [1, 2], [1, 2, 3], [1, 2, 3, 4], [1, 2, 3, 4, 5]]

通过每次创建新的 ArrayList 对象

List<List<Integer>> result = new ArrayList<>();

    for(int i = 1; i <= 5; ++i){

      List<Integer> list = new ArrayList<>();

      for(int j = 1; j <= i; ++j){
        list.add(j);
      }
      result.add(list);
    }
    System.out.println(result);

我得到了想要的输出
[[1], [1, 2], [1, 2, 3], [1, 2, 3, 4], [1, 2, 3, 4, 5]]

但是,当尝试使用 clear() 时没有得到相同的结果(以避免在每次迭代中创建新对象)并且 result Arraylist 在调用 clear() 时也会变空list Arraylist

List<Integer> list = new ArrayList<>();
List<List<Integer>> result = new ArrayList<>();

    for(int i = 1; i <= 5; ++i){
      for(int j = 1; j <= i; ++j){
        list.add(j);
      }
      result.add(list);
      list.clear();
    }
    System.out.println(result);

我使用 clear() 得到以下输出
[[], [], [], [], []] --> 不是我想要的输出

如何在不每次都创建新对象(ArrayList 对象)并使用 clear() 或任何其他概念的情况下实现所需的输出。

是否result and list ArrayList 指向相同的引用或有一些其他原因导致输出不正确。请让我知道关于 ArrayList 主题我不知道或缺少的东西。

tl;博士

你说:

Are result and list ArrayList pointing to the same reference

是的。

要添加副本,请更改:

result.add( list );

… 对此:

result.add( new ArrayList<>( list ) );

详情

您可能认为 result.add(list); 正在添加 list 内容 。但是不,该调用传递了您命名为 listArrayList 对象的 reference(指针,内存地址)。 list 的内容与该调用无关。无论是空的还是满的,列表本身,即容器,就是您要传递给 add.

的内容

所以您将同一个列表添加了五次。 result 的所有元素都指向同一个列表。如果添加到包含的列表,result 的所有五个元素都会看到该更改。如果清除添加的 listresult 的所有五个元素都会看到该更改,result 的所有五个元素都指向同一个现在为空的列表。

要添加不同的 unmodifiable lists,调用 List.copyOf

result.add( List.copyOf( list ) );

要添加不同的可修改列表,请构造新的 ArrayList 对象。将现有列表传递给新列表的构造函数。

result.add( new ArrayList<>( list ) );

您更改为使用相同的列表,恰恰会产生您所看到的后果。您多次将同一个列表添加到 result 列表,最后一次 clear() 调用导致结果列表的所有元素显示为空。

您可以通过将反映当前内容的列表副本添加到结果来解决此问题,但结果比您已有的原始解决方案效率低,在每个循环的主体中创建一个新列表迭代,无需额外的复制操作。

当您想要包含不同内容的列表时,没有办法绕过创建不同的列表对象。但是您可以避免在这种特定情况下使用不同的存储空间:

List<List<Integer>> result = new ArrayList<>();
List<Integer> list = new ArrayList<>();
for(int i = 1; i <= 5; i++) list.add(i);
for(int j = 1; j <= 5; j++) result.add(list.subList(0, j));
System.out.println(result);

这将首先创建最大的列表,它将实际存储 Integer 个对象。第二个循环中使用的 subList 操作在这个列表中创建了一个 view,没有自己的存储空间,只是不同的边界。

这也意味着当您之后修改原始 list 时,这可能会改变结果(当您 set 元素时)或使子列表无效(当您添加或删除元素时).

您可以通过使用不可变列表来避免这种情况

List<List<Integer>> result = new ArrayList<>();
List<Integer> list = new ArrayList<>();
for(int i = 1; i <= 5; i++) list.add(i);
list = List.copyOf(list);
for(int j = 1; j <= 5; j++) result.add(list.subList(0, j));
System.out.println(result);

这只对不可变列表进行一次复制操作(需要 Java 10),而不是在每次循环迭代中进行。 subList 返回的列表也是不可变的。

从JDK16开始,创建不可变列表时甚至可以避免临时ArrayList和复制操作

List<List<Integer>> result = new ArrayList<>();
List<Integer> list = IntStream.rangeClosed(1, 5).boxed().toList();
for(int j = 1; j <= 5; j++) result.add(list.subList(0, j));
System.out.println(result);

虽然 Stream API 可能有初始开销,但不会通过保存仅五个元素的复制操作来补偿。


适用于所有变体的一个小改进是不对最后一个元素使用 subList,它与整个列表具有相同的内容,即代替

for(int j = 1; j <= 5; j++) result.add(list.subList(0, j));

你可以使用

for(int j = 1; j < 5; j++) result.add(list.subList(0, j));
result.add(list);