Iterating Collections in Java
Master Java collection iteration: Iterator and ListIterator interfaces, for-each loop, fail-fast behavior, and concurrent modification.
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
Calling list.remove(item) directly during iteration is the mistake that trips up most developers. It changes the collection’s modCount but leaves the iterator’s expectedModCount untouched, so the next next() call sees the mismatch and throws ConcurrentModificationException. The solution is to call iterator.remove() instead — this keeps both the collection and the iterator in sync.
Filter a list mid-iteration by checking a condition on each element and removing it via the iterator when the condition is true. The example below drops every entry matching “b” or “c”:
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
ListIterator adds backward movement and positional edits to the basic iterator contract. Its cursor tracks two things: the element you last received from next() (or previous()) and the position right before the current element. Call next() and the cursor slides forward; call previous() and it slides back. At the current position you can call set() to replace the last returned element or add() to insert a new one before the cursor.
Only ListIterator lets you modify a list mid-traversal without triggering ConcurrentModificationException. This makes it the right tool for list editors, token parsers, and any algorithm that needs to revisit or rewrite earlier positions.
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
forEachRemaining() is a default method on Iterator (Java 8+) that runs a Consumer against every element left in the iterator. It replaces the boilerplate while (it.hasNext()) { action(it.next()); } pattern with a single call. Once called, the iterator is exhausted — there is no resuming from where you left off.
A useful pattern is partial consumption: advance the iterator manually with a few next() calls to skip the head of a collection, then hand the remainder to forEachRemaining(). This lets you skip expensive first elements without discarding the already-obtained iterator instance.
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
Sets behave differently from lists during iteration. Since sets have no indexed access, your only options are the enhanced for-each loop or an explicit iterator. The more important distinction is which Set implementation determines the order:
HashSetgives O(1) operations but iteration order is unpredictable — it can differ from insertion order.LinkedHashSetremembers insertion order, so traversal follows the order elements were added.TreeSetkeeps elements sorted by their natural ordering or by a suppliedComparator.
Maps expose three separate views: entrySet(), keySet(), and values(). Iterate entrySet() when you need both key and value — it avoids a per-element map lookup. keySet() is fine when you only need the keys. values() is for when the keys are irrelevant.
// 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
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`."
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
- ArrayList — common target when iterating over lists
- HashMap — iterating over map entries and keys
- Queue and Deque — iteration patterns for queues
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.