【问题标题】:Is removing elements from an ArrayList or LinkedList while iterating through it with a for loop in Java bad? If so, why?在使用 Java 中的 for 循环进行迭代时从 ArrayList 或 LinkedList 中删除元素是否不好?如果是这样,为什么?
【发布时间】:2020-07-15 13:28:46
【问题描述】:

我正在向某人展示我的代码,他们说这会导致未定义的行为。作为一名 Java 程序员,这不是我所理解的。在下面的代码块中,我将遍历 scenes,它是一个 ArrayList,并从中删除元素。

for(int i = 0; i < scenes.size() - 1; i++)
    {
        if(!(Double.valueOf(scenes.get(i + 1)) - Double.valueOf(scenes.get(i)) > 10))
        {
            scenes.remove(i + 1);
            i--;
        }
    }

这会编译并且不会在运行时抛出异常,但我仍然不确定它是否是编程禁忌,为什么它是编程禁忌,以及正确的做法是什么。我听说过使用 Iterator.remove() 以及创建一个全新的 List

【问题讨论】:

  • 这里有几样东西要解压。首先,链表的基于索引的访问需要O(i) 时间,因为要到达特定索引,您必须从其头部(或尾部,以最接近的一个,假设它具有此优化)开始遍历每个节点。一般来说,迭代列表的更好方法是使用它的Iterator,因为它会根据内部实现以最佳方式实现。
  • 然后,当您使用Iterator 时,它会在您修改列表后抛出ConcurrentModificationException - 修改后,在修改之前创建的大多数迭代器会变得陈旧并抛出异常,如果您在那种状态下使用它们。 Iterator.remove() 不会使该特定迭代器过时,但会使所有其他迭代器过时。
  • @jnorman,这是关于 python 的问题,不一定是 Java。

标签: java list iterator concurrentmodification


【解决方案1】:

ArrayList 中,从列表中间删除一个元素需要您将所有具有较高索引的元素向下移动一个。如果你做一次(或少量)这很好,但如果你重复做,效率会很低。

您也不想为此使用Iterator,因为Iterator.remove() 也存在同样的问题。

更好的方法是遍历列表,将要保留的元素移动到新位置;然后在最后删除列表的尾部:

int dst = 0;
for (int src = 0; src < scenes.size(); ++dst) {
  // You want to keep this element.
  scenes.set(dst, scenes.get(src++));

  // Now walk along the list until you find the element you want to keep.
  while (src < scenes.size()
         && Double.parseDouble(scenes.get(src)) - Double.parseDouble(scenes.get(dst)) <= 10) {
    // Increment the src pointer, so you won't keep the element.
    ++src;
  }
}

// Remove the tail of the list in one go.
scenes.subList(dst, scenes.size()).clear();

ArrayList.removeIf 使用的是这种“移位和清除”方法;您不能在此处直接使用它,因为您无法检查列表中的相邻元素,您只能访问当前元素)。


您可以采用类似的方法,该方法也可以有效地处理非随机访问列表,例如LinkedList。您需要避免重复调用getset,因为这些是例如O(size)LinkedList 的情况下。

在这种情况下,您将使用 ListIterator 而不是普通索引:

ListIterator<String> dst = scenes.listIterator();
for (ListIterator<String> src = scenes.listIterator(); src.hasNext();) {
  dst.next();
  String curr = src.next();
  dst.set(curr);

  while (src.hasNext()
         && Double.parseDouble(src.next()) - Double.parseDouble(curr) <= 10) {}
}
scenes.subList(dst.nextIndex(), scenes.size()).clear();

或者类似的东西。我没有测试过,ListIterator 使用起来总是很混乱。

【讨论】:

  • 需要注意的是,这个问题不仅仅与ArrayList有关,还与LinkedList有关。由于基于索引的访问,这种移位方法的实现不适用于链表,但如果使用两个迭代器,效果会更好。
  • 好的,我已经阅读了您的答案几次,虽然我仍在努力理解其中的一些内容,但我只想提一下,在我研究这个问题时,我遇到了卡内基梅隆大学说以下内容的大学页面。 “根据 Iterator.remove() 方法的 Java API 文档:如果在迭代过程中以任何方式而不是通过调用此方法来修改底层集合,则迭代器的行为是未指定的”。这似乎表明使用 Iterator 不应该遇到您在开始时提到的相同问题。
  • @brendanw36 它根本没有这么说。这所说的甚至与使用Iterator.remove() 方法无关,它是在谈论使用List.remove 同时也在迭代。
  • @brendanw36 一种简单的方法来说服自己从ArrayList 中删除Iterator.remove() 是低效的:创建一个大小为n 的列表(例如new ArrayList&lt;&gt;(Collections.nCopies(size, 0)));用for (Iterator&lt;?&gt; it = list.iterator(); it.hasNext(); it.next(), it.remove());(从头删除)从其中删除所有内容需要多长时间,将其与while (!list.isEmpty()) list.remove(list.size() - 1);(从末尾删除)进行比较。试试 n=1000000。
  • @AndyTurner 所以访问数组是 O(1),但插入和删除是 O(n)。访问 LinkedList 是 O(n),但插入和删除是 O(1)。你是说我可以通过使用带有 ListIterator 的 LinkedList 进行访问,两全其美?
【解决方案2】:

这很简单,适用于 ArrayList 或 LinkedList:

        Iterator<String> iterator = list.iterator();
        double current = 0;
        double next;
        boolean firstTime = true;
        while (iterator.hasNext()) {
            if (firstTime) {
                current = Double.parseDouble(iterator.next());
                firstTime = false;
            } else {
                next = Double.parseDouble(iterator.next());
                if (next - current > 10) {
                    current = next;
                } else {
                    iterator.remove();
                }
            }
        }

【讨论】:

    猜你喜欢
    • 2019-11-03
    • 2010-11-05
    • 1970-01-01
    • 2016-09-27
    • 2013-08-25
    • 2012-01-16
    • 2019-11-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多