How Java’s Enhanced for Loop Works with Collections
Learn how Java’s enhanced for loop works behind the scenes through Iterable and Iterator, why directly modifying a collection during iteration can cause ConcurrentModificationException, and which approaches to use instead.
A deep dive into
Iterable,Iterator, fail-fast behavior,ConcurrentModificationException, and the exact cases where removing elements inside a for-each loop silently corrupts your data.
1. The starting point: Collection extends Iterable
When I first studied Java Collections, I noticed a small but meaningful inheritance relationship:
At first glance, this looks like a trivial detail. But this single line is the reason the enhanced for loop works with every collection in the framework — List, Set, Queue, Deque, and so on.
To understand why, we need to look at what Iterable actually promises.
2. What Iterable and Iterator actually promise
Iterable exposes one abstract method:
That's it. Iterable says: "I can give you an Iterator that walks through my elements."
Iterator is the walker itself:
A few important details:
next()throwsNoSuchElementExceptionif there are no more elements.remove()is a default method (since Java 8) that throwsUnsupportedOperationExceptionunless the iterator overrides it.remove()must be called afternext(), and only once pernext()call — otherwise it throwsIllegalStateException.
So the contract is small but strict. The enhanced for loop is built on top of it.
3. How the enhanced for loop is desugared for Iterable
The Java Language Specification (§14.14.2) defines the enhanced for loop over an Iterable as equivalent to:
So when you write:
The compiler translates it to:
The loop does not touch the collection directly. It only talks to the Iterator. That single fact is what causes every problem we're about to discuss.
4. The one exception: arrays
The desugaring above applies to Iterable. Arrays are handled differently. The JLS specifies that for arrays:
is translated to an indexed loop:
No Iterator. No Iterator.remove(). So when someone claims "the for-each loop always uses an iterator," that's only true for Iterable — not for arrays. Keep this distinction in mind: the rest of this article is about Iterable (and therefore about every Collection).
5. Why the loop cannot safely modify the collection
Now consider the natural next step — removing an element during iteration:
This looks harmless. But it produces:
The name is misleading. This is not about multiple threads. It happens on a single thread because the loop's Iterator and the list disagree about the state of the collection.
The Iterator keeps its own small piece of state — its position in the list. The list keeps its own state — its size and its internal array. When you call names.remove(...) directly, you modify the list without telling the iterator. The iterator then tries to keep walking based on stale assumptions. Java detects this mismatch and refuses to continue.
To understand exactly how the detection works, we need to look at fail-fast iterators.
6. Fail-fast iterators: modCount and expectedModCount
ArrayList (and most collections in java.util) does not just track its contents — it tracks how many times it has been structurally modified. That count is a field called modCount, inherited from AbstractList.
Each Iterator captures that count at the moment it is created:
The idea is simple:
modCount— the collection's current "version number," incremented whenever the collection is structurally modified (elements added or removed).expectedModCount— the iterator's snapshot of that version, taken when the iterator was created.- On every
next(), the iterator checks: has the collection changed behind my back? If yes, it throwsConcurrentModificationException.
This is called fail-fast: the iterator fails quickly and loudly rather than returning garbage data or producing non-deterministic behavior later.
7. The subtlety: hasNext() does not check modCount
Look closely at ArrayList.Itr above. hasNext() only does:
There is no checkForComodification() call inside hasNext(). Only next() performs that check.
This is critical. It means that a direct structural modification during iteration does not always throw. Whether you get an exception or silent misbehavior depends entirely on whether the loop ends up calling next() again after the modification.
That single detail explains all three behaviors we'll see next: exception, clean exit, and silent skip.
8. Walkthrough: what happens if you remove the first, middle, or last element?
Let's trace the exact code paths with three cases. In every example:
The iterator starts with cursor = 0, expectedModCount = m.
Case A — Remove the first element ("Ali")
| Step | Iteration call | Result |
|---|---|---|
| 1 | hasNext() → 0 != 3 → true | |
| 2 | next() → returns "Ali", cursor = 1 | |
| 3 | Match → names.remove("Ali") | modCount = m+1, size = 2, list becomes ["Ahmed", "Sabeer"] |
| 4 | hasNext() → 1 != 2 → true | |
| 5 | next() → checkForComodification() → m+1 != m | throws ConcurrentModificationException |
Case B — Remove the middle element ("Ahmed")
| Step | Iteration call | Result |
|---|---|---|
| 1 | hasNext() → 0 != 3 → true | |
| 2 | next() → returns "Ali", cursor = 1 | No match |
| 3 | hasNext() → 1 != 3 → true | |
| 4 | next() → returns "Ahmed", cursor = 2 | |
| 5 | Match → names.remove("Ahmed") | modCount = m+1, size = 2, list becomes ["Ali", "Sabeer"] |
| 6 | hasNext() → 2 != 2 → false | |
| 7 | Loop ends. "Sabeer" is never visited | No exception — but a silent bug |
Case C — Remove the last element ("Sabeer")
| Step | Iteration call | Result |
|---|---|---|
| 1 | next() → "Ali", cursor = 1 | No match |
| 2 | next() → "Ahmed", cursor = 2 | No match |
| 3 | next() → "Sabeer", cursor = 3 | |
| 4 | Match → names.remove("Sabeer") | modCount = m+1, size = 2, list becomes ["Ali", "Ahmed"] |
| 5 | hasNext() → 3 != 2 → true | |
| 6 | next() → checkForComodification() → m+1 != m | throws ConcurrentModificationException |
Summary of the three cases
| Removed element | Outcome |
|---|---|
| First | ConcurrentModificationException |
| Middle (second-to-last) | Loop terminates early — last element silently skipped |
| Last | ConcurrentModificationException |
Both outcomes are bugs. One throws. One quietly produces wrong results. Neither is acceptable. The only safe way to remove during iteration is through the iterator itself or through APIs that use the iterator correctly.
9. What "structural modification" actually means
Only structural changes invalidate an iterator. On ArrayList:
- ✅ Structural:
add,addAll,remove,removeAll,retainAll,clear,removeIf. - ❌ Not structural:
set(index, value),get,contains,size.
So this is fine:
set does not touch modCount, so the iterator's snapshot remains valid.
On HashMap, structural changes are put with a new key, or remove of an existing key. Replacing the value of an existing key is not structural.
10. Concurrent collections: Java has two collection packages
Up to this point, every behavior we've discussed — fail-fast iterators, modCount, expectedModCount, ConcurrentModificationException — belongs to one package: java.util. That is the package that contains ArrayList, HashMap, HashSet, and most of the collections you use day to day.
But Java does not have just one collection framework. It has two.
| Package | Purpose | Examples | Iterator behavior |
|---|---|---|---|
java.util | General-purpose collections, single-threaded by default | ArrayList, HashMap, HashSet, LinkedList, TreeMap | Fail-fast — throws ConcurrentModificationException on structural modification during iteration (best-effort) |
java.util.concurrent | Thread-safe collections for concurrent code | CopyOnWriteArrayList, ConcurrentHashMap, ConcurrentLinkedQueue, BlockingQueue implementations | Snapshot or weakly consistent — never throws ConcurrentModificationException |
These two packages were designed with different goals:
java.utilprioritizes speed in single-threaded code. Fail-fast iteration is cheap because it only compares two integers (modCountandexpectedModCount).java.util.concurrentprioritizes safe concurrent access. The trade-off is that iteration does not reflect a single fixed snapshot — it reflects whatever state the collection happens to be in as you walk through it.
When someone says "modifying a collection during iteration throws ConcurrentModificationException," they are really saying "modifying a fail-fast collection in java.util during iteration throws ConcurrentModificationException." The rule does not extend to java.util.concurrent.
10.1 The java.util behavior: fail-fast iterators
Example: ArrayList.
The iterator keeps a snapshot of the list's modCount (its version number). Every time next() is called, it compares that snapshot with the current modCount. If they differ, it throws ConcurrentModificationException.
11. Safe ways to remove elements during iteration
There are exactly two patterns you should use. Both are safe. Both have their own edge cases.
11.1 Use Iterator.remove() explicitly
Why this works: ArrayList.Itr.remove() does two things that the naive list.remove(...) does not.
The iterator updates its own cursor and refreshes its expectedModCount after the removal. The iterator stays in sync with the list. No exception, no skipped element.
11.2 Use removeIf() for simple conditions
removeIf is cleaner and expresses intent. Internally, ArrayList.removeIf is optimized: it scans once, builds a bitmap of the elements to delete, then compacts the array in a single pass. It does not use the public Iterator at all for ArrayList. For LinkedList it uses the iterator's remove(). Either way, it is safe.
11.3 Pitfalls of both approaches
Both Iterator.remove() and removeIf() can throw UnsupportedOperationException on collections that don't support removal. Common cases:
List.of(...)— immutable.Arrays.asList(array)— fixed-size (canset, cannotadd/remove).Collections.unmodifiableList(...)— read-only view.Collections.emptyList()— no elements, no removal.
So "use removeIf" is correct advice — but only when the collection actually supports removal.
Iterator.remove() also throws IllegalStateException if you call it:
- before the first
next(), or - twice in a row without an intervening
next().
That's because remove() removes the last returned element — there has to be one, and it has to be fresh.
12. The mental model
Chain the pieces together in your head:
And when you modify a fail-fast collection during iteration:
So when you write:
Think: "Java is running an iterator behind the scenes."
And when you need to modify the collection during iteration:
- Simple conditional removal →
removeIf(...) - Removal with side effects or complex conditions → explicit
Iterator+it.remove() - Direct structural modification during a for-each over a fail-fast collection → ❌ never
13. Key takeaways
CollectionextendsIterable. That's why the enhancedforloop works with collections.- For
Iterable, the enhancedforloop is desugared into an iterator loop. For arrays, it is desugared into an indexed loop — no iterator. - Fail-fast iterators use
modCount/expectedModCount. Onlynext()checks it;hasNext()does not. - Direct modification can throw or silently skip. Both are bugs. Which one you get depends on the position of the removed element relative to the iterator's cursor and the list's new size.
- Structural modification ≠ every modification.
set()is fine during iteration;add/removeare not. ConcurrentModificationExceptionis best-effort. It is not guaranteed for all collections.java.util.concurrentiterators are weakly consistent and never throw it.- The two safe ways to remove are
Iterator.remove()andremoveIf(). Both can throwUnsupportedOperationExceptionon immutable or fixed-size collections. Iterator.remove()must be called at most once pernext(), or it throwsIllegalStateException.
The enhanced for loop is not magic. It is a thin, elegant veneer over Iterator. Once you see the iterator underneath, every behavior — including the confusing ones like ConcurrentModificationException — becomes predictable.
