我得到了这段代码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
(除非使用Iterator
的remove()
方法,否则这是非法的(。这就是并发修改:在同一数据结构上有两个不同的视角。如果从一个视角(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发生。