Iterating Collections in Java
Master Java collection iteration: Iterator and ListIterator interfaces, for-each loop, fail-fast behavior, and concurrent modification.
Iterating Collections in Java
Java provides multiple ways to iterate over collections, each with different performance characteristics and safety guarantees. Understanding the iteration mechanisms — and their failure modes — is essential for writing correct, performant collection code.
Introduction
Every Java collection eventually needs to be traversed — and getting iteration wrong is a source of real production bugs. The ConcurrentModificationException that crashes a server mid-request happens when a collection is structurally modified (add, remove, clear) while being iterated with a standard fail-fast iterator. The fix is always the same pattern: use iterator.remove() instead of collection.remove(). But understanding why requires knowing how the iterator’s internal expectedModCount tracks the collection’s modCount — and where that tracking fails to catch concurrent modifications.
Beyond the concurrent modification problem, iteration performance varies dramatically across collection types. ArrayList iteration is cache-friendly — elements sit in contiguous memory and the CPU prefetches efficiently. LinkedList iteration is pointer-chasing — each node is scattered in memory, and the CPU cannot prefetch what it has not yet reached. The difference shows up as 2-5x slower traversal in practice, often misidentified as a network or computation bottleneck.
This post covers the iteration mechanisms available in Java: Iterator for forward traversal with safe removal, ListIterator for bidirectional traversal and positional modifications, enhanced for-each for clean read-only traversal, and forEach() (Java 8+) for functional-style operations. You will see the fail-fast vs fail-safe distinction, the mechanics of concurrent modification detection, and the performance trade-offs that differentiate array-backed collections from linked ones.
When to Use Each Iteration Style
| Method | Best For | Avoid When |
|---|---|---|
for loop with index | Lists where you need the index or modify during iteration | Any Collection without random access |
Enhanced for-each | Simple, read-only iteration | You need to remove during iteration |
Iterator.remove() | Safe removal during iteration | You need bidirectional traversal |
ListIterator | Bidirectional traversal, modifications at arbitrary positions | General Collection (only works on List) |
forEach() (Java 8+) | Functional-style operations on collections | You need to throw checked exceptions |
The Iterator Interface
public interface Iterator<E> {
boolean hasNext();
E next();
void remove(); // Optional, removes last element returned by next()
default void forEachRemaining(Consumer<? super E> action) { }
}
Fail-Fast vs Fail-Safe
Java’s standard collection iterators (from ArrayList, HashSet, etc.) are fail-fast: they detect concurrent modification and throw ConcurrentModificationException. This is a best-effort detection — not a guarantee — because the modCount check happens only at unsafe points.
Fail-safe iterators (from ConcurrentHashMap, CopyOnWriteArrayList, etc.) work on a copy of the collection and will not throw ConcurrentModificationException.
Mermaid Diagram: Iterator and Collection Relationship
sequenceDiagram
participant Client
participant Iterator
participant Collection
Client->>Collection: iterator()
Collection->>Iterator: create Iterator(cursor=0, lastReturned=null)
Iterator-->>Client: Iterator instance
loop while it.hasNext()
Client->>Iterator: next()
Iterator->>Collection: expectedModCount == modCount?
Collection-->>Iterator: true
Iterator->>Iterator: advance cursor, update lastReturned
Iterator-->>Client: element
end
alt concurrent modification detected
Iterator->>Client: ConcurrentModificationException
end
Failure Scenarios
| Scenario | Cause | Result |
|---|---|---|
ConcurrentModificationException | Structural modification during iteration | Runtime crash |
NoSuchElementException | Calling next() when hasNext() is false | Runtime crash |
IllegalStateException | Calling remove() before next() or twice | Runtime crash |
IndexOutOfBoundsException | ListIterator with invalid index | Runtime crash |
Trade-Off Table
| Iteration Method | Complexity | Safe Remove | Bidirectional | Fail-Fast |
|---|---|---|---|---|
for loop | O(n) | No | N/A | N/A |
| For-each loop | O(n) | No | No | Yes (via iterator) |
Iterator.remove() | O(n) | Yes | No | Yes |
ListIterator | O(n) | Yes | Yes | Yes |
forEach() (functional) | O(n) | No | No | Yes |
ConcurrentHashMap iterator | O(n) | Yes | No | Fail-safe |
Code Snippets
Safe Removal with Iterator
List<String> list = new ArrayList<>(List.of("a", "b", "c", "d"));
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.equals("b") || s.equals("c")) {
it.remove(); // Safe — uses iterator's internal state
}
}
// list = ["a", "d"]
ListIterator for Bidirectional Traversal
List<String> list = new ArrayList<>(List.of("x", "y", "z"));
ListIterator<String> it = list.listIterator();
it.next(); // "x"
it.next(); // "y"
it.previous(); // "y"
it.set("Y"); // Replace "y" with "Y"
it.add("new"); // Insert after "Y"
// list = ["x", "Y", "new", "z"]
forEachRemaining with Lambda
List<Integer> nums = List.of(1, 2, 3, 4, 5);
Iterator<Integer> it = nums.iterator();
it.forEachRemaining(n -> {
if (n % 2 == 0) System.out.println(n); // 2, 4
});
Iterating Different Collection Types
// Set — no guaranteed order
Set<String> set = new HashSet<>(List.of("a", "b", "c"));
for (String s : set) { System.out.println(s); }
// Map — iterate entries, keys, or values
Map<String, Integer> map = new HashMap<>();
map.put("key1", 1);
map.put("key2", 2);
for (Map.Entry<String, Integer> e : map.entrySet()) {
System.out.println(e.getKey() + " = " + e.getValue());
}
Observability Checklist
- Monitor
ConcurrentModificationExceptionoccurrences — they indicate structural modifications during iteration - Profile iteration time in hot paths — especially for
LinkedListwhere random access is O(n) per index - Track
forEach()call patterns — functional iteration can allocate lambda objects in hot paths - Check for repeated
.iterator()calls in loops — prefer creating the iterator once and reusing it - Log iteration patterns over
ConcurrentHashMapto detect potential race conditions
Security Notes
- Fail-safe iterators work on copies — be aware that iterating over a snapshot may miss concurrent updates
- When iterating over collections containing sensitive data, ensure the iteration does not leak elements through
toString(), logging, or exception messages CopyOnWriteArrayListcreates a full copy on each mutation — it is safe for read-heavy workloads but expensive for write-heavy ones- Avoid serializing iterators — they do not carry meaningful state that survives deserialization
Common Pitfalls / Anti-Patterns
- Calling
remove()beforenext(): ThrowsIllegalStateException— you must callnext()at least once before callingremove() - Using for-each for removal: The for-each loop hides the iterator, so you cannot call
remove()directly — use an explicit iterator - Modifying during
forEachRemaining: Can throwConcurrentModificationExceptionif the collection is structurally modified remove()vsremoveAll()during iteration: Callingcollection.removeAll(collection)while iterating over the same collection triggersConcurrentModificationException- Assuming order in HashSet iteration: HashSet does not guarantee iteration order; use
LinkedHashSetfor insertion-order orTreeSetfor sorted-order iteration
Quick Recap
- Use
Iteratorfor forward iteration with safe removal - Use
ListIteratorfor bidirectional traversal and modifications at arbitrary positions - Use
for-eachfor simple read-only loops - Use
forEach()(Java 8+) for functional-style operations - Standard iterators are fail-fast — they detect and throw on concurrent modification
- Always prefer
iterator.remove()overcollection.remove()when iterating
Interview Questions
Model Answer: "It is thrown when a collection is **structurally modified** (add, remove, clear) while iterating over it with a fail-fast iterator. The iterator maintains an internal `expectedModCount` field set at iterator creation. If `modCount` (the collection's modification counter) differs, a concurrent modification is assumed and the exception is thrown. This is a **best-effort detection** — it is not guaranteed to catch all concurrent modifications."
Model Answer: "`Iterator` is unidirectional (forward only) and works on any `Collection`. `ListIterator` extends `Iterator` and adds **bidirectional** traversal, positional `add()`, `set()`, and `remove()` operations, and requires a `List` to operate on. `ListIterator` also has access to the current index via `nextIndex()` and `previousIndex()`."
Model Answer: "Use the iterator's own `remove()` method:
Iterator<E> it = collection.iterator();\nwhile (it.hasNext()) {\n if (condition(it.next())) {\n it.remove(); // Safe — updates internal state\n }\n}Do not call `collection.remove()` directly — this bypasses the iterator's internal tracking and triggers `ConcurrentModificationException`."
Model Answer: "Fail-fast iterators **fail quickly and cleanly** when they detect that the underlying collection has been modified during iteration. They throw `ConcurrentModificationException` rather than proceeding with undefined or corrupted behavior. This is a safety feature, not a guarantee — some modifications may slip through undetected. Concurrent access requires explicitly concurrent collection types (`ConcurrentHashMap`, `CopyOnWriteArrayList`, etc.)."
Model Answer: "The enhanced `for-each` loop (`for (E e : collection)`) uses an `Iterator` under the hood — it desugars to the same `hasNext()`/`next()` pattern. The `forEach()` method on `Iterable` (Java 8+) is a **default method** on the `Iterable` interface that accepts a `Consumer`. Both are fail-fast, but `forEach()` enables functional programming style and can be easily parallelized with `parallelStream()`."
Model Answer: "`Iterable` is the interface that objects must implement to be usable in a for-each loop — it provides the `iterator()` method. `Iterator` is the object that actually traverses the collection, with `hasNext()`, `next()`, and `remove()` methods. Implementing `Iterable` does not require implementing `Iterator` (the default `forEachRemaining()` handles it)."
Model Answer: "`iterator()` returns a unidirectional `Iterator
Model Answer: "`IllegalStateException` is thrown when `remove()` is called before `next()` (no element to remove), or when `remove()` is called twice without a subsequent `next()` call. Each `remove()` must be preceded by exactly one `next()`. Calling `remove()` after `previous()` is equally valid."
Model Answer: "`ConcurrentHashMap` iterators snapshot the state at creation — they iterate over a point-in-time view of the map's entry set. Concurrent modifications are ignored by the iterator (not reflected in the iteration). This avoids `ConcurrentModificationException` but means the iterator may not reflect the most recent state."
Model Answer: "`toArray()` returns an `Object[]` array — the runtime type is always `Object[]`, not the actual element type. `toArray(T[] a)` populates the provided array with elements, creating a typed array. If the collection fits in the given array, it is returned; otherwise, a new array of the same runtime type is allocated."
Model Answer: "`Enumeration` is the **legacy** interface (Java 1.0) predating `Iterator` (Java 1.2). Use `Enumeration` only when working with legacy APIs that return `Enumeration` (like `Vector.elements()` or `Hashtable.keys()`). For all new code, prefer `Iterator` — it has `remove()` and shorter method names."
Model Answer: "`forEachRemaining(action)` performs the given action on each remaining element in the iteration, then exhausts the iterator. It is more convenient than a manual while loop: `iterator.forEachRemaining(System.out::println)`. Internally, it repeatedly calls `next()` and applies the action until no elements remain."
Model Answer: "Fail-fast iterators maintain an internal `expectedModCount` set at creation. Before each `next()` call, the iterator compares `expectedModCount` with the collection's current `modCount`. If they differ (structural modification detected), `ConcurrentModificationException` is thrown. This is a best-effort detection, not a guarantee."
Model Answer: "`iterator.remove()` is safe — it updates the iterator's internal state to match the collection's modification. `collection.remove()` during iteration bypasses the iterator's tracking, causing `expectedModCount` to diverge from `modCount`, triggering `ConcurrentModificationException` on the next iterator operation."
Model Answer: "The exception is only thrown at **iterator safepoints** — before `next()` and `remove()`. If you modify the collection and then call `iterator.next()`, the exception fires. But if you modify the collection and then call `collection.size()` or `collection.isEmpty()`, no exception is thrown. The detection is not a guarantee."
Model Answer: "Both have the same O(1) per-element iteration complexity. `ListIterator` has additional overhead for maintaining bidirectional state (previous/next cursor position). For unidirectional forward iteration, `Iterator` is slightly more efficient. `ListIterator` is required only when backward traversal or positional modification is needed."
Model Answer: "`Iterable.forEach()` (Java 8+) is a default method on the `Iterable` interface that accepts a `Consumer`. The enhanced for-each (`for (E e : collection)`) uses an `Iterator` under the hood. Both traverse all elements, but `forEach()` enables functional-style operations and can be easily parallelized with `parallelStream()`."
Model Answer: "Calling `iterator.remove()` removes the last element returned by `next()` — safe and deterministic. Calling `collection.removeAll(collection)` from within an iteration loop triggers `ConcurrentModificationException` because the collection is modified structurally outside the iterator. Use `iterator.remove()` for safe mid-iteration removal."
Model Answer: "The enhanced for-each (`for (E e : collection)`) desugars to `for (Iterator
Model Answer: "`Enumeration` is the legacy interface from Java 1.0 (older, pre-dates Iterator from Java 1.2). Iterator has `remove()` and shorter method names. `Enumeration` is used only when working with legacy APIs that return it (e.g., `Vector.elements()`, `Hashtable.keys()`). For all new code, use Iterator. The main difference is that `Enumeration` does not support `remove()` and has method names like `hasMoreElements()` and `nextElement()` instead of `hasNext()` and `next()`."
Further Reading
- Oracle Iterator Documentation — Official Iterator interface specification
- Oracle ListIterator Documentation — Bidirectional iterator interface
- Baeldung: Iterator vs Iterable — Understanding the difference between the two interfaces
- Java SE: For Each Loop — How the enhanced for loop works with collections
Conclusion
Iteration is where many Java collection performance issues surface in production. The core rule is simple: never modify a collection directly during iteration — use iterator.remove() or copy before iterating. The fail-fast mechanism catches some violations but not all, so defensive copying or explicit iterators are the reliable approach.
For most cases, an explicit Iterator with remove() is the clearest pattern. ListIterator is the right tool when you need backward traversal or positional modifications. Functional forEach() is clean for read-only operations but does not mix well with checked exceptions or mid-iteration mutations.
Iteration patterns apply to every collection type — HashMap, TreeMap, ArrayList, LinkedList, HashSet, TreeSet, and Queue/Deque all share the same underlying iterator contracts. Once iteration mechanics are solid, HashMap entry iteration and Queue processing patterns become straightforward to reason about.
Category
Related Posts
Abstract Classes in Java
Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.
Arithmetic Operators in Java
Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.
Array Basics in Java
Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.