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
实例创建之后(这是非法的,除非你使用Iterator
的 remove()
方法)。这就是并发修改:对同一个数据结构有两种不同的看法。如果从一个角度 (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。
我得到了这段代码片段,它运行良好。
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
实例创建之后(这是非法的,除非你使用Iterator
的 remove()
方法)。这就是并发修改:对同一个数据结构有两种不同的看法。如果从一个角度 (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。