英文:
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("test");
s.add(null);
代码会抛出 ConcurrentModificationException
异常。
所以我的问题是,为什么如果 null 是列表中的第一个对象,我可以移除它,但如果它是第二个对象就不行?
英文:
I got this code Snippet which is working fine.
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);
}
But after changing
s.add(null);
s.add("test");
to
s.add("test");
s.add(null);
The code throws a ConcurrentModificationException
So my question is why can I remove null if it's the first object in the list but not if it's the second one?
答案1
得分: 4
以下是翻译好的部分:
第一点要理解的是,代码无效,因为它在迭代过程中结构上修改了列表,而这是不允许的(注意:这是一个轻微的简化,因为有可允许的方法来修改列表,但这不是其中之一)。
第二点要理解的是,“ConcurrentModificationException”是基于最佳努力的原则运行的:
快速失败迭代器会尽力抛出“ConcurrentModificationException”。因此,编写依赖于此异常的程序来确保其正确性是错误的:迭代器的快速失败行为只应用于检测错误。
因此,无效的代码可能会引发或不引发“ConcurrentModificationException” - 这真的取决于Java运行时,并可能取决于平台、版本等。
例如,当我在本地尝试 [test, null, null]
时,我不会收到异常,但结果也是无效的 [test, null]
。这与您观察到的两种行为不同。
有几种方法可以修复您的代码,其中最常见的可能是使用 Iterator.remove()
。
英文:
The first thing to understand is that the code is invalid, as it structurally modifies the list while iterating over it and this is not permitted (NB: this is a slight simplification, as there are pemissible ways to modify a list, but this is not one of them).
The second thing to understand is that ConcurrentModificationException
works on a best-effort basis:
> 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.
And so invalid code may or may not raise ConcurrentModificationException
-- it's really up to the Java runtime and could depend on the platform, version etc.
For example, when I try [test, null, null]
locally, I get no exception but also an invalid result, [test, null]
. This is a different behaviour to the two behaviours that you observed.
There are several ways to fix your code, the most common of which is probably to use Iterator.remove()
.
答案2
得分: 1
以下是您要翻译的内容:
在最佳情况下,ConcurrentModificationException
应该在两种情况下抛出。但事实并非如此(请参阅下面来自文档的引用)。
如果您使用 for-each 循环遍历一个 Iterable
,则数据结构的 iterator()
方法返回的 Iterator
将在内部使用(for-each 循环只是语法糖)。
现在,您不应该在创建此 Iterator
实例之后对正在被迭代的 Iterable
进行结构性修改(除非您使用 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 ArrayList 文档):
该类的 iterator 和 listIterator 方法返回的迭代器是快速失败的:如果列表在迭代器创建之后以任何方式在结构上进行修改,除了通过迭代器自己的 remove 或 add 方法之外,迭代器将抛出 ConcurrentModificationException。因此,在面对并发修改时,迭代器会迅速干净地失败,而不是在未来某个不确定的时间冒险出现任意的非确定性行为。
请注意,迭代器的快速失败行为不能得到保证,因为一般来说,在未同步的并发修改存在的情况下,不可能作出任何硬性保证。快速失败迭代器只是尽力而为地抛出 ConcurrentModificationException。因此,编写依赖此异常来保证其正确性的程序是错误的:迭代器的快速失败行为只应用于检测错误。
英文:
Well, in a best case scenario, the ConcurrentModificationException
should be thrown in any of the two cases. But it isn't (see the quote from the docs below).
If you use the for-each loop to iterate over an Iterable
, the Iterator
returned by the iterator()
method of the data structure will be used internally (the for-each loop is just syntactic sugar).
Now you shouldn't (structurally) modify an Iterable
that is being iterated after this Iterator
instance was created (it's illegal unless you use the Iterator
's remove()
method). This is what a concurrent modification is: There are two different perspectives on the same data structure. If it's modified from one perspective (list.remove(object)
), the other perspective (the Iterator) won't be aware of this.
It is not about the element being null
. The same happens if you change the code to remove the string:
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);
}
Now the reason this behaviour differs in some scenarios is simply the following (from the 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.
答案3
得分: 0
这个异常是因为在遍历列表时删除了元素。为了解决这个问题,您可以像这样使用迭代器:
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");
}
}
英文:
This exception occurred because of remove element while iterating on a list. To solve this problem you can use iterator like this:
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");
}
}
答案4
得分: 0
异常行为发生是因为迭代器的实现方式。for each 循环将使用 ArrayList.iterator() 来遍历集合。
Iterator<Object> obj = s.iterator();
while(obj.hasNext()){
Object t = obj.next();
// 你的其余代码
}
在第一种情况下,正常工作时的输出是:
[null, test]
null
ObjectRemoved = null
[test]
这意味着最后一个迭代被跳过了。从 ArrayList 源代码中,我们可以看到为什么会发生这种情况。
public boolean hasNext() {
return cursor != size;
}
在第一次迭代中,next
被调用并且光标更新为 1。然后,null 被移除。在后续调用 hasNext
时,光标等于大小,循环停止。最后一个迭代被跳过,但没有引发 CME(ConcurrentModificationException)
现在,在第二种情况下。null 是列表中的最后一项,在循环再次开始时,hasNext
被调用,它返回 true,因为光标大于列表的大小!然后在调用 Iterator.next
时发生 CME。
英文:
The odd behavior happens because of the way the iterator is implemented. The for each loop is going to use the ArrayList.iterator() to iterate over the collection.
Iterator<Object> obj = s.iterator();
while(obj.hasNext()){
Object t = obj.next();
// the rest of the code you have.
}
In the first case that works your output is.
>[null, test]
null
ObjectRemoved = null
[test]
This means the last iteration was skipped. From the ArrayList source code we can see why this is.
public boolean hasNext() {
return cursor != size;
}
On your first iteration next
is called and the cursor is updated to 1. Then the null is removed. On the subsequent call to hasNext
the cursor equals the size and the loop stops. Missing the last iteration, but not throwing a CME
Now, in your second case. The null is the last item in the list, when the loop goes to start again, hasNext
is called and it returns true because the cursor is greater than the size of the list! Then the CME occurs when Iterator.next
is called.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论