Crazy. You found a bug in java. Such simple code - it boggles the mind.
Please refer to the source code of ArrayList. The bug is, specifically, in ArrayList.java on line 962.
Ordinarily, arraylists have a so-called 'mod counter'. Anytime you modify the arraylist in any way (be it adding, removing, clearing, etc), the modcounter is incremented by one.
Whenever you invoke .iterator()
(which for (String item :list)
also does at the very beginning, once), a new iterator object is made (see line 947 in the above link), and this iterator object stores the modcount as it was when the iterator is made.
The idea is that all iterator methods (which aren't many; only hasNext
, next
and remove
) will first check if the modcount of the backing arraylist (the arraylist you got the iterator from by invoking its .iterator()
method) is different from the remembered modcount, and if yes, the iterator will instant-abort with a ConcurrentModificationException
.
The bug is that the hasNext method fails to do this. I think it's an optimization but it has led to a bug.
Thus, this bizarre interaction occurs:
List<String> list = new ArrayList<String>();
list has modcounter = 0.
list.add("1");
list.add("2");
modcounter is now 2.
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
This is syntax sugar. javac compiles it as if it read:
Iterator<String> it$1 = list.iterator();
while (it$1.hasNext()) {
String item = it$1.next();
// your actual code inside the for loop here:
if ("1".equals(item)) {
list.remove(item);
}
}
So let's go through it on that basis:
Iterator<String> it$1 = list.iterator();
An iterator object is made; its expectedModCount
field is set to 2
, as that is the current modcount of list
.
while (it$1.hasNext()) {
the position field of the iterator is 0, and the size of the backing list is 2. So, yes, there are more values to return. hasNext()
returns true, the while loop will enter.
String item = it$1.next();
modcounter is checked. expectedModCount
of the iterator is 2, the mod counter of the list is 2, so that check passes, item
is set to "1"
, and the iterator's position field is incremented, so that is now 1.
if ("1".equals(item)) {
list.remove(item);
}
The item is indeed "1", so list.remove(item)
is called. list updates its modcount to 3, its size to 1, and removes the "1"
element from its backing array.
and now the weirdness ensues:
while (it$1.hasNext()) {
well, hasNext() does not check that expectedModCount
of the iterator is still equal to the modCount
of the list. If it had, this would have failed, but it doesn't do that. The iterator's position field is 1
, and the list's size is also 1
, so hasNext()
returns false, and the while loop exits. That's it: We're out of the loop without ever hitting a ConcurrentModificationException.
In contrast, in the second snippet, you invoke next()
twice on it$1
and then the element is removed. At that point, hasNext() is invoked, and then the iterator's position
field is 2, the list's size is 1, and the specific check (line 962 of ArrayList) checks if listSize != iteratorPosition
. It's not, thus, hasNext returns true (a bit odd). Therefore the while loop enters the body a third time, runs String item = it$1.next()
, and the next()
method does do a modCount check. expectedModCount
is 2, list's modCount
is 3, thus, CoModEx is thrown.
To reproduce this, you need to remove the second-to-last element on an arraylist like this. In your example list of 2 elements, that'd be the first element.
How do you actually remove items during iteration?
The right strategy is to use the remove
method of iterator. You can't access the iterator in a for(:)
loop, so write:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String item = it.next();
if ("2".equals(item)) {
it.remove(); // this is how to do it!
}
}
remove() on an iterator is unique: Assuming the modcount check passes, it increments both the iterator's expected modcount as well as the backing list's mod count: It is the only way you can modify a list during iteration without having the iterator fail on you (well, that, and this bug you found).
Next steps
I'll file a bug over at openjdk for this issue.