ConcurrentModificationException:如果它不是第一个变量,为什么删除列表中的 null 会引发此异常



我得到了这段代码Snippet,它运行得很好。

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在尽力而为的基础上工作:

失败快速迭代器在尽力而为的基础上抛出ConcurrentModificationException。因此,编写一个依赖于此异常的正确性的程序是错误的:迭代器的快速故障行为应该只用于检测错误。

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

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

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

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

如果使用for each循环迭代Iterable,则数据结构的iterator()方法返回的Iterator将在内部使用(for each环路只是语法糖(。

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

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

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);
}

现在,这种行为在某些场景中不同的原因只是以下(来自用于ArrayList的Java SE 11文档(:

此类迭代器和listIterator方法返回的迭代器是故障快速的:如果在迭代器创建后的任何时候,以除迭代器自己的remove或add方法之外的任何方式对列表进行结构修改,则迭代器将抛出ConcurrentModificationException。因此,面对并发修改,迭代器会快速而干净地失败,而不是冒着在未来不确定的时间出现任意、不确定行为的风险。

请注意,迭代器的故障快速行为无法得到保证,因为一般来说,在存在不同步的并发修改的情况下,不可能做出任何硬保证。故障快速迭代器在尽力而为的基础上抛出ConcurrentModificationException。因此,编写一个依赖于此异常的正确性的程序是错误的:迭代器的快速故障行为应该只用于检测错误。

由于在列表上迭代时移除元素,因此发生此异常。为了解决这个问题,你可以使用这样的迭代器:

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");
}
}

由于迭代器的实现方式,会出现奇怪的行为。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,并且它返回true,因为光标大于列表的大小!当调用CCD_ 21时,CME发生。

最新更新