ConcurrentModificationException:如果它不是第一个变量,为什么删除 List 中的 null 会抛出此异常

ConcurrentModificationException: Why does removing the null in List throw this Exception if it´s not the first variable

我得到了这段代码片段,它运行良好。

import java.util.ConcurrentModificationException;
import java.util.*;

ArrayList<Object> s = new ArrayList<>();

s.add(null);
s.add("test");

try {
    System.out.println(s + "\n");
    for (Object t : s) {
        System.out.println(t);

        if (t == null || t.toString().isEmpty()) {
            s.remove(t);
            System.out.println("ObjectRemoved = " + t + "\n");
        }

    }

    System.out.println(s + "\n");
} catch (ConcurrentModificationException e) {
    System.out.println(e);
} catch (Exception e) {
    System.out.println(e);
}

但改变后

s.add(null);
s.add("test");

s.add("test");
s.add(null);

代码抛出 ConcurrentModificationException

所以我的问题是,如果 null 是列表中的第一个对象,为什么我可以删除它,如果它是第二个对象,我就不能删除它?

首先要理解的是代码是无效的,因为它在迭代列表时在结构上修改了列表,这是不允许的(注意:这是一个轻微的简化,因为有允许的方法来修改列表,但这不是其中之一)。

第二个要理解的是 ConcurrentModificationException 在 best-effort 的基础上工作:

Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.

因此无效代码可能会或可能不会引发 ConcurrentModificationException -- 这实际上取决于 Java 运行时并且可能取决于平台、版本等。

例如,当我在本地尝试 [test, null, null] 时,没有出现异常,但结果也是无效的 [test, null]。这与您观察到的两种行为不同。

有几种方法可以修复您的代码,其中最常见的可能是 to use Iterator.remove()

此异常是由于在列表上迭代时删除元素而发生的。要解决此问题,您可以像这样使用迭代器:

for (Iterator iterator = s.iterator(); iterator.hasNext(); ) {
    Object eachObject = iterator.next();
    if (eachObject == null || eachObject.toString().isEmpty()) {
        iterator.remove();
        System.out.println("ObjectRemoved = " + eachObject + "\n");
    }
}

嗯,在最好的情况下,ConcurrentModificationException 应该在两种情况中的任何一种情况下抛出。但事实并非如此(请参阅下面文档中的引述)。

如果使用for-each循环迭代Iterable,数据结构的iterator()方法返回的Iterator将在内部使用( for-each 循环只是 syntactic sugar).

现在你不应该(在结构上)修改正在迭代的 Iterable 这个 Iterator 实例创建之后(这是非法的,除非你使用Iteratorremove() 方法)。这就是并发修改:对同一个数据结构有两种不同的看法。如果从一个角度 (list.remove(object)) 修改它,则另一个角度 (迭代器) 将不会意识到这一点。

不是关于元素是null。如果您更改代码以删除字符串,也会发生同样的情况:

ArrayList<Object> s = new ArrayList<>();

s.add("test");
s.add(null);

try {
    System.out.println(s + "\n");
    for (Object t : s) {
        System.out.println(t);

        if (t != null && t.equals("test")) {
            s.remove(t);
            System.out.println("ObjectRemoved = " + t + "\n");
        }

    }

    System.out.println(s + "\n");
} catch (ConcurrentModificationException e) {
    System.out.println(e);
} catch (Exception e) {
    System.out.println(e);
}

现在,这种行为在某些情况下有所不同的原因很简单(来自 Java SE 11 Docs for ArrayList):

The iterators returned by this class's iterator and listIterator methods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.

Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.

由于迭代器的实现方式,会出现奇怪的行为。 for each 循环将使用 ArrayList.iterator() 遍历集合。

Iterator<Object> obj = s.iterator();

while(obj.hasNext()){
    Object t = obj.next();
    // the rest of the code you have.
}

在第一种情况下,您的输出是。

[null, test]
null
ObjectRemoved = null
[test]

这意味着最后一次迭代被跳过。从ArrayList源代码我们可以看出这是为什么。

public boolean hasNext() {
    return cursor != size;
} 

在您的第一次迭代中调用 next 并将光标更新为 1。然后删除 null。在随后调用 hasNext 时,游标等于大小并且循环停止。 缺少最后一次迭代,但没有抛出 CME

现在,在你的第二种情况下。 null 是列表中的最后一项,当循环再次开始时,调用 hasNext 并且它 returns true 因为游标大于列表的大小!然后在调用 Iterator.next 时发生 CME。