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.