It is mere fluke that you don't get a ConcurrentModificationException
with HashSet
. The reason for that fluke is that you happen to add the element on the last iteration of the loop.
This is far from guaranteed to happen, because HashSet
has no ordering guarantees. Try adding or removing some more elements from the list, and you'll find that eventually be able to get a ConcurrentModificationException
, because the 40
won't be the last element in the list.
The simplest way to reproduce this is to work with just a single element (since then the ordering of the HashSet
is known):
HashSet<Integer> hashSet = new HashSet<>();
hashSet.add(1);
for (int i : hashSet) {
hashSet.add(2);
}
This won't throw a ConcurrentModificationException
.
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
for (int i : list) {
list.add(2);
}
This will throw a ConcurrentModificationException
.
As to the reason why you don't get the exception when adding to a HashSet
on the last iteration,
look at the source code for the iterator returned by HashSet.iterator()
:
abstract class HashIterator {
// ...
public final boolean hasNext() {
return next != null;
}
// ...
}
So, the hasNext()
value is precomputed, based on next
; this is set a bit lower down in the code:
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
// ^ Here
}
return e;
}
So, the value of hasNext
is determined when the previous element of the iteration is determined.
Since your code is:
Iterator it = marks.iterator();
while (it.hasNext()) {
int value = it.next();
if (value == 40)
marks.add(50);
}
the hasNext()
is called after the loop iteration in which the element is added; but next
was set to null when it.next()
was called - before the call to add
- and so hasNext()
will return false, and the loop exits without ConcurrentModificationException
.
On the other hand, the iterator of ArrayList
uses the size of the underlying list to determine the return value of hasNext()
:
public boolean hasNext() {
return cursor != size;
// ^ current position of the iterator
// ^ current size of the list
}
So this value is "live" with respect to increasing (or decreasing) the size of the underlying list. So, hasNext()
would return true after adding the value; and so next()
is called. But the very first thing that next()
does is to check for comodification:
public E next() {
checkForComodification();
// ...
And thus the change is detected, and a ConcurrentModificationException
is thrown.
Note that you wouldn't get a ConcurrentModificationException
with ArrayList
if you modified it on the last iteration, if you were to add and remove an element (or, rather, to add and remove equal numbers of elements, so that the list's size is the same afterwards).