Iterating Collections in Java

Master Java collection iteration: Iterator and ListIterator interfaces, for-each loop, fail-fast behavior, and concurrent modification.

published: reading time: 13 min read author: GeekWorkBench

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

MethodBest ForAvoid When
for loop with indexLists where you need the index or modify during iterationAny Collection without random access
Enhanced for-eachSimple, read-only iterationYou need to remove during iteration
Iterator.remove()Safe removal during iterationYou need bidirectional traversal
ListIteratorBidirectional traversal, modifications at arbitrary positionsGeneral Collection (only works on List)
forEach() (Java 8+)Functional-style operations on collectionsYou 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

ScenarioCauseResult
ConcurrentModificationExceptionStructural modification during iterationRuntime crash
NoSuchElementExceptionCalling next() when hasNext() is falseRuntime crash
IllegalStateExceptionCalling remove() before next() or twiceRuntime crash
IndexOutOfBoundsExceptionListIterator with invalid indexRuntime crash

Trade-Off Table

Iteration MethodComplexitySafe RemoveBidirectionalFail-Fast
for loopO(n)NoN/AN/A
For-each loopO(n)NoNoYes (via iterator)
Iterator.remove()O(n)YesNoYes
ListIteratorO(n)YesYesYes
forEach() (functional)O(n)NoNoYes
ConcurrentHashMap iteratorO(n)YesNoFail-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 ConcurrentModificationException occurrences — they indicate structural modifications during iteration
  • Profile iteration time in hot paths — especially for LinkedList where 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 ConcurrentHashMap to 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
  • CopyOnWriteArrayList creates 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

  1. Calling remove() before next(): Throws IllegalStateException — you must call next() at least once before calling remove()
  2. Using for-each for removal: The for-each loop hides the iterator, so you cannot call remove() directly — use an explicit iterator
  3. Modifying during forEachRemaining: Can throw ConcurrentModificationException if the collection is structurally modified
  4. remove() vs removeAll() during iteration: Calling collection.removeAll(collection) while iterating over the same collection triggers ConcurrentModificationException
  5. Assuming order in HashSet iteration: HashSet does not guarantee iteration order; use LinkedHashSet for insertion-order or TreeSet for sorted-order iteration

Quick Recap

  • Use Iterator for forward iteration with safe removal
  • Use ListIterator for bidirectional traversal and modifications at arbitrary positions
  • Use for-each for 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() over collection.remove() when iterating

Interview Questions

1. What causes `ConcurrentModificationException`?

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."

2. What is the difference between `Iterator` and `ListIterator`?

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()`."

3. How do you safely remove elements while iterating?

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`."

4. What does "fail-fast" mean in the context of Java iterators?

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.)."

5. How does `forEach()` differ from enhanced for-each in Java 8+?

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()`."

6. What is the difference between `Iterator` and `Iterable`?

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)."

7. How does `listIterator()` differ from `iterator()`?

Model Answer: "`iterator()` returns a unidirectional `Iterator` starting at the beginning. `listIterator()` returns a `ListIterator` which is bidirectional and allows forward/backward traversal, index tracking via `nextIndex()`/`previousIndex()`, and modification via `set()`, `add()`, and `remove()` at arbitrary positions."

8. What causes `IllegalStateException` when calling `iterator.remove()`?

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."

9. How does `ConcurrentHashMap` achieve fail-safe iteration?

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."

10. What is the difference between `toArray()` and `toArray(T[] a)`?

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."

11. When should you use `Enumeration` instead of `Iterator`?

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."

12. What is the purpose of the `forEachRemaining()` method in Iterator?

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."

13. How does iterator fail-fast behavior protect against concurrent modification?

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."

14. What is the difference between `Iterator.remove()` and `Collection.remove()` during iteration?

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."

15. When does `ConcurrentModificationException` not get thrown despite concurrent modification?

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."

16. What is the performance characteristic of `ListIterator` compared to `Iterator`?

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."

17. How does `Iterable.forEach()` method differ from the enhanced for-each loop?

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()`."

18. What is the difference between `remove()` and `removeAll()` when called from an iterator?

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."

19. How does Java's for-each loop desugar to iterator logic?

Model Answer: "The enhanced for-each (`for (E e : collection)`) desugars to `for (Iterator i = collection.iterator(); i.hasNext(); ) { E e = i.next(); ... }`. The compiler performs this transformation automatically. For arrays, it desugars to an index-based loop instead."

20. What is the difference between Enumeration and Iterator, and when should you use each?

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

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.

#java-abstract-classes #java #java-fundamentals

Arithmetic Operators in Java

Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.

#java-arithmetic-operators #java #java-fundamentals

Array Basics in Java

Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.

#java-array-basics #java #java-fundamentals