【问题标题】:Why clear() on one ArrayList is clearing elements in another ArrayList?为什么一个 ArrayList 上的 clear() 会清除另一个 ArrayList 中的元素?
【发布时间】:2021-11-07 15:37:15
【问题描述】:

我想要的输出应该是:
[[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() 时不会得到相同的结果(以避免在每次迭代中创建一个新对象),并且在 list Arraylist 上调用 clear()result 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 主题的内容。

【问题讨论】:

  • “结果和列表 ArrayList 是否指向同一个引用” - 是的,完全正确。
  • 如果不每次都创建新对象(ArrayList 对象),您将无法获得所需的输出。这些列表是不同的,因此它们不能用同一个列表来表示。
  • 这样想。您多次将同一个列表添加到父列表并在完成后将其清除,现在当您得到一个空列表列表时您会感到惊讶吗?如果您想要多个列表而不是多次使用同一个列表,则需要创建新列表。
  • "如何在不创建新对象(ArrayList 对象)的情况下实现所需的输出" 这是不可能的。
  • @LouisWasserman 也许,这是一个正确的术语问题。确实,没有办法创建新对象(具有不同的身份),但是这个任务does not require distinct storage,这意味着我们可以避免多次处理列表或填充新列表。

标签: java arraylist collections


【解决方案1】:

tl;博士

你说:

结果和列表ArrayList是否指向同一个引用

是的。

要添加副本,请更改:

result.add( list );

…到这个:

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

详情

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

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

要添加不同的unmodifiable lists,请致电List.copyOf

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

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

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

【讨论】:

  • 另外,请注意,如果您曾经使用过List.subList(),您获得的不是新切片,而是原始列表的视图。更改子列表并更改原始列表。为防止这种情况发生,请按照此答案中的说明将其传递给构造函数。
  • 我认为,通过new ArrayList&lt;&gt;( list ) 复制一个列表,然后从头开始重新填充list,效率低于 OP 的第一个 sn-p,后者在每次迭代中创建并填充一个新列表。如果循环没有使用clear(),而只是在复制之前添加了一个元素,情况会有所不同。但是,即使这样,也可以用an even more efficient variant, not bearing copy operations at all 代替。
【解决方案2】:

您使用相同列表的更改,正是您所看到的结果。您多次将同一个列表添加到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 返回的列表也是不可变的。

从 JDK 16 开始,你甚至可以避免临时的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);

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-11-04
    • 1970-01-01
    • 2021-05-15
    • 1970-01-01
    • 1970-01-01
    • 2015-07-20
    • 2020-07-21
    相关资源
    最近更新 更多