首页 > 解决方案 > java list.remove 适用于第一个元素,但不适用于其他元素

问题描述

众所周知,不能在 foreach 中使用 list.remove。让我感到困惑的是,阶段 1 运行正常,但阶段 2 不是。有人可以解释一下吗?

阶段1

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
for (String item : list) {
    if ("1".equals(item)) {
        list.remove(item);
    }
}
System.out.println(list);

阶段2

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
for (String item : list) {
    if ("2".equals(item)) {
        list.remove(item);
    }
}
System.out.println(list);

标签: javacollections

解决方案


疯狂的。你在java中发现了一个错误。如此简单的代码 - 它令人难以置信。

请参考 ArrayList 的源代码。具体来说,该错误位于第 962 行的 ArrayList.java 中

通常,arraylists 有一个所谓的“mod counter”。任何时候您以任何方式修改数组列表(无论是添加、删除、清除等),modcounter 都会加一。

每当您调用时.iterator()for (String item :list)在最开始时也是一次),都会创建一个新的迭代器对象(请参见上面链接中的第 947 行),并且该迭代器对象会像创建迭代器时一样存储 modcount。

这个想法是所有迭代器方法(不是很多;只有hasNext,nextremove)将首先检查后备数组列表(通过调用其.iterator()方法获得迭代器的数组列表)的 modcount 是否与记住的 modcount 不同,如果是的,迭代器将立即中止ConcurrentModificationException.

错误是hasNext 方法无法做到这一点。我认为这是一种优化,但它导致了一个错误。

因此,发生了这种奇怪的交互:

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

列表有 modcounter = 0。

list.add("1");
list.add("2");

modcounter现在是2。

for (String item : list) {
    if ("1".equals(item)) {
        list.remove(item);
    }
}

这是语法糖。javac 将其编译为如下内容:

Iterator<String> it$1 = list.iterator();
while (it$1.hasNext()) {
    String item = it$1.next();
    // your actual code inside the for loop here:
    if ("1".equals(item)) {
        list.remove(item);
    }
}

因此,让我们在此基础上进行一下:

Iterator<String> it$1 = list.iterator();

制作了一个迭代器对象;它的expectedModCount字段设置为2,因为那是 的当前 modcount list

while (it$1.hasNext()) {

迭代器的位置字段是 0,后备列表的大小是 2。所以,是的,有更多的值要返回。hasNext()返回true,将进入while循环。

String item = it$1.next();

modcounter 被检查。expectedModCount迭代器的值为 2,列表的 mod 计数器为 2,因此检查通过,item设置为"1",并且迭代器的位置字段递增,因此现在为 1。

if ("1".equals(item)) {
    list.remove(item);
}

该项目确实是“1”,因此list.remove(item)被称为。list 将其 modcount 更新为 3,将其大小更新为 1,并"1"从其支持数组中删除该元素。

现在奇怪的事情发生了:

while (it$1.hasNext()) {

好吧, hasNext() 不检查迭代expectedModCount器是否仍然等于modCount列表。如果有,这将失败,但它不会那样做。迭代器的位置字段是1,列表的大小也是1,所以hasNext()返回false,退出while循环。就是这样:我们在没有遇到 ConcurrentModificationException 的情况下退出了循环。

相反,在第二个片段中,您调用next()了两次it$1,然后元素被删除。此时, hasNext() 被调用,然后迭代器的position字段为 2,列表的大小为 1,具体检查(ArrayList 的第 962 行)检查 if listSize != iteratorPosition。因此,hasNext 不是返回true(有点奇怪)。因此,while 循环第三次进入主体,运行String item = it$1.next(),并且该next()方法确实进行了 modCount 检查。expectedModCount是 2,列表modCount是 3,因此,抛出 CoModEx。

要重现这一点,您需要像这样删除数组列表中的倒数第二个元素。在您的 2 个元素的示例列表中,这将是第一个元素。

您如何在迭代期间实际删除项目?

正确的策略是使用remove迭代器的方法。您不能在for(:)循环中访问迭代器,因此请编写:

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String item = it.next();
    if ("2".equals(item)) {
        it.remove(); // this is how to do it!
    }
}

迭代器上的 remove() 是唯一的:假设 modcount 检查通过,它会增加迭代器的预期 modcount 以及后备列表的 mod 计数:这是您可以在迭代期间修改列表而不会让迭代器失败的唯一方法(嗯,那个,还有你发现的这个错误)。

下一步

对于这个问题,我将在 openjdk 上提交一个错误。


推荐阅读