Iterationmedium3-5 years

A loop that removes expired sessions from a `List<Session>` inside a `for-each` sometimes throws `ConcurrentModificationException` and sometimes silently leaves one expired session behind, with no exception, on the same codebase. There's only one thread. Explain both outcomes from the same mechanism.

for (Session s : sessions) compiles to a loop over sessions.iterator(), and every structural change to the list — including one made through the list itself, not through the iterator, exactly what sessions.remove(s) inside the loop does — increments a counter called modCount. The iterator captured modCount's value when it was created and checks it against the live value on every call to next(); if they disagree, it throws ConcurrentModificationException, and no second thread is required — the name is misleading, one thread modifying the list it's currently iterating is enough, and it's overwhelmingly the common case. What explains the inconsistent behavior is that the check happens in next(), not in hasNext() — hasNext() only compares the iterator's cursor position to the list's current size. Removing the second-to-last element shrinks the list so that the cursor now equals the new size, hasNext() correctly (if misleadingly) returns false, and the loop exits cleanly before next() ever gets a chance to notice the mismatch — silently skipping whatever element the cursor would have pointed at next. Remove any other element and hasNext() still returns true, so next() runs, sees the mismatched count, and throws. Same bug, same single thread, two different outcomes depending purely on which element happens to trigger the removal.

The lesson behind it →