Senior 5 min · March 05, 2026

Java Iterator/ListIterator — Silent Data Loss from remove()

Silent data loss: list.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Iterator is the base interface for forward-only, safe traversal and removal over any Collection.
  • ListIterator extends Iterator for List types only: bidirectional traversal, set(), add(), and index awareness.
  • Performance: Iterator/ListIterator over a LinkedList is O(n) — index-based for-loop is O(n^2).
  • Production insight: calling list.remove() inside a for-each loop throws ConcurrentModificationException instantly.
  • Biggest mistake: assuming ListIterator.add() inserts after the cursor — it actually inserts before.
Plain-English First

Imagine you're reading a book. An Iterator is like a bookmark that only lets you move forward — you start at page 1 and flip through to the end, one page at a time. A ListIterator is a smarter bookmark: it lets you flip forward AND backward, jump to a specific page, and even scribble notes (modify the book) while you read. Java's collections work the same way — Iterator is your basic forward-only reader, ListIterator is your full-featured editor.

Every real application eventually needs to walk through a collection of data — a shopping cart, a list of users, a queue of tasks. Java gives you several ways to do this, but Iterator and ListIterator are the tools the language itself uses under the hood. The enhanced for-loop you write every day? That compiles down to an Iterator. Understanding what's really happening gives you control that the for-each loop simply can't provide.

The problem they solve is surprisingly nuanced. You can't safely remove an element from an ArrayList while looping over it with a regular for loop — the indexes shift and you skip elements or get an IndexOutOfBoundsException. You can't traverse a LinkedList backwards without an index (which defeats the point of a linked list). Iterator and ListIterator were designed specifically to solve safe, cursor-based traversal and modification of collections — they give you a protocol, not just a loop.

By the end of this article you'll know exactly when to reach for Iterator instead of for-each, when ListIterator's bidirectional power is worth it, how to safely remove or replace elements mid-traversal, and how to avoid the dreaded ConcurrentModificationException. You'll also have concrete answers for the interview questions that trip up even experienced developers.

What Iterator Actually Is — and Why It Exists

Iterator is an interface in java.util. It defines a contract: any class that implements it promises to let you step through its elements one at a time. The three methods are tiny but powerful: hasNext() tells you if there's another element waiting, next() hands you that element and advances the cursor, and remove() deletes the last element returned by next() from the underlying collection — safely.

The key word there is safely. When you use Iterator.remove(), the iterator and the collection stay in sync. The iterator knows it just removed something and adjusts its internal state accordingly. That's the entire reason Iterator exists — not just to traverse, but to let you traverse and mutate without blowing up.

Every collection in the Java Collections Framework implements the Iterable interface, which has one job: return an Iterator. When you write a for-each loop, the compiler calls iterator() on your collection and calls hasNext() and next() behind the scenes. This means Iterator isn't some advanced feature — it's the beating heart of iteration in Java. Knowing it explicitly just gives you the steering wheel.

TaskQueueIterator.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class TaskQueueIterator {

    public static void main(String[] args) {

        // A simple task queue — imagine these are pending support tickets
        List<String> taskQueue = new ArrayList<>();
        taskQueue.add("Send welcome email");
        taskQueue.add("URGENT: Fix login bug");
        taskQueue.add("Update user profile");
        taskQueue.add("URGENT: Payment gateway down");
        taskQueue.add("Archive old records");

        System.out.println("=== Processing Task Queue ===");

        // Grab an iterator from the list — the cursor starts BEFORE the first element
        Iterator<String> taskIterator = taskQueue.iterator();

        while (taskIterator.hasNext()) {          // hasNext() peeks — doesn't consume
            String currentTask = taskIterator.next(); // next() consumes and advances cursor

            System.out.println("Checking: " + currentTask);

            // Business rule: remove completed urgent tasks from the live queue
            // Using iterator.remove() — NOT taskQueue.remove() — keeps iterator in sync
            if (currentTask.startsWith("URGENT")) {
                System.out.println("  -> Escalating and removing from queue: " + currentTask);
                taskIterator.remove(); // SAFE removal — no ConcurrentModificationException
            }
        }

        System.out.println("\n=== Remaining Tasks ===");
        // For-each is fine here since we're done modifying
        for (String remainingTask : taskQueue) {
            System.out.println("  - " + remainingTask);
        }
    }
}
Output
=== Processing Task Queue ===
Checking: Send welcome email
Checking: URGENT: Fix login bug
-> Escalating and removing from queue: URGENT: Fix login bug
Checking: Update user profile
Checking: URGENT: Payment gateway down
-> Escalating and removing from queue: URGENT: Payment gateway down
Checking: Archive old records
=== Remaining Tasks ===
- Send welcome email
- Update user profile
- Archive old records
Watch Out: iterator.remove() vs list.remove()
Never call list.remove() while iterating with an Iterator. Always call iterator.remove(). The iterator tracks its own position — calling remove() on the collection directly invalidates that internal state and throws ConcurrentModificationException on the next hasNext() or next() call.
Production Insight
We've seen production incidents where engineers used list.remove() inside a for-each loop.
The removal often succeeds for the first matching element but shifts subsequent elements.
Rule: if you need to remove while iterating, always use iterator.remove() or collect-then-apply.
Key Takeaway
Iterator.remove() keeps the cursor and collection in sync.
For-each hides the Iterator, so you lose remove() access.
Explicit Iterator is the only safe way to remove during traversal.

ListIterator — The Two-Way, Full-Control Iterator

ListIterator extends Iterator and is only available on List implementations (ArrayList, LinkedList, Vector). It adds five critical capabilities that plain Iterator doesn't have: backward traversal with hasPrevious() and previous(), positional awareness with nextIndex() and previousIndex(), element replacement with set(), and element insertion with add().

Think about when you'd actually need this. A text editor's undo/redo history is a perfect example — you need to walk forward through actions to redo them, and backward to undo them. A music playlist that supports both next-track and previous-track is another. Any scenario where you're editing a list in place — replacing certain values based on conditions — becomes dramatically cleaner with ListIterator.set() compared to tracking index variables manually.

The cursor model of ListIterator is worth understanding precisely. The cursor sits BETWEEN elements, not ON them. After calling next(), the cursor has moved past one element — calling previous() returns that same element again. This sounds confusing but it's logically consistent once you visualize the cursor as a gap between elements rather than a pointer to one.

PlaylistManager.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

public class PlaylistManager {

    public static void main(String[] args) {

        List<String> playlist = new ArrayList<>();
        playlist.add("Intro Track");
        playlist.add("Main Theme");
        playlist.add("Bridge");
        playlist.add("Main Theme");  // duplicate — will be replaced
        playlist.add("Outro");

        System.out.println("=== Original Playlist ===");
        playlist.forEach(track -> System.out.println("  " + track));

        // Get a ListIterator starting at the BEGINNING (index 0)
        ListIterator<String> playlistEditor = playlist.listIterator();

        System.out.println("\n=== Editing Playlist (Forward Pass) ===");

        while (playlistEditor.hasNext()) {
            // nextIndex() tells us WHERE we are before we consume the element
            int position = playlistEditor.nextIndex();
            String track = playlistEditor.next(); // advance cursor, get element

            System.out.println("Position " + position + ": " + track);

            // Replace duplicate 'Main Theme' at position 3 with 'Reprise'
            if (track.equals("Main Theme") && position == 3) {
                playlistEditor.set("Main Theme (Reprise)"); // replaces in-place, no index needed
                System.out.println("  -> Replaced with: Main Theme (Reprise)");
            }
        }

        System.out.println("\n=== Rewinding (Backward Pass) ===");

        // Cursor is now at the END — walk backwards using hasPrevious()
        while (playlistEditor.hasPrevious()) {
            int position = playlistEditor.previousIndex();
            String track = playlistEditor.previous(); // move cursor back, get element
            System.out.println("Position " + position + ": " + track);
        }

        System.out.println("\n=== Final Playlist ===");
        playlist.forEach(track -> System.out.println("  " + track));
    }
}
Output
=== Original Playlist ===
Intro Track
Main Theme
Bridge
Main Theme
Outro
=== Editing Playlist (Forward Pass) ===
Position 0: Intro Track
Position 1: Main Theme
Position 2: Bridge
Position 3: Main Theme
-> Replaced with: Main Theme (Reprise)
Position 4: Outro
=== Rewinding (Backward Pass) ===
Position 4: Outro
Position 3: Main Theme (Reprise)
Position 2: Bridge
Position 1: Main Theme
Position 0: Intro Track
=== Final Playlist ===
Intro Track
Main Theme
Bridge
Main Theme (Reprise)
Outro
Pro Tip: Start ListIterator at Any Index
list.listIterator() starts at position 0, but list.listIterator(int index) starts the cursor at any position you choose. This is powerful for resumable processing — save the index, shut down, restart, and pick up exactly where you left off. No manual index tracking required.
Production Insight
In a production log processing pipeline, we used ListIterator.listIterator(index) to resume batch processing after a crash.
The index was persisted to a database, allowing exactly-once semantics without locks.
This pattern avoided reprocessing millions of log lines.
Key Takeaway
ListIterator gives bidirectional traversal, set(), add(), and index awareness.
Cursor sits between elements — design your add/set calls accordingly.
Resumable iteration via listIterator(index) is a powerful production pattern.

ConcurrentModificationException — The Most Common Iterator Trap

This exception is the number-one pain point developers hit when they first work with iterators. It occurs when a collection is structurally modified (elements added or removed) through the collection itself while an iterator over it is active. Java's fail-fast iterators detect this using an internal modCount — a counter that increments on every structural change. The iterator captures this count when created, and on every call to next() it checks: 'has this count changed since I was born?' If yes, it throws ConcurrentModificationException immediately.

This is intentional. Java is telling you: 'you're modifying the collection while iterating — your iterator's internal state is now invalid and I'd rather crash loudly than give you silent data corruption.' It's a safety net, not a bug.

The fix is always to route modifications through the iterator itself (iterator.remove(), listIterator.add(), listIterator.set()) or to collect changes and apply them after iteration completes. The latter approach — called 'collect then apply' — is the right choice when you need to add elements, since plain Iterator has no add() method.

SafeCollectionModification.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class SafeCollectionModification {

    public static void main(String[] args) {

        List<Integer> temperatures = new ArrayList<>();
        temperatures.add(72);
        temperatures.add(85);
        temperatures.add(91);  // heatwave threshold
        temperatures.add(68);
        temperatures.add(95);  // heatwave threshold

        System.out.println("=== BROKEN: This will throw ConcurrentModificationException ===");
        try {
            for (Integer temp : temperatures) {
                if (temp > 90) {
                    temperatures.remove(temp); // modifying the list WHILE for-each iterates it
                }
            }
        } catch (java.util.ConcurrentModificationException e) {
            System.out.println("Caught: " + e.getClass().getSimpleName());
            System.out.println("Reason: for-each hides an Iterator — we modified the list externally");
        }

        // Reset the data
        temperatures.clear();
        temperatures.add(72);
        temperatures.add(85);
        temperatures.add(91);
        temperatures.add(68);
        temperatures.add(95);

        System.out.println("\n=== FIXED: Use iterator.remove() for safe removal ===");
        Iterator<Integer> tempIterator = temperatures.iterator();
        while (tempIterator.hasNext()) {
            int reading = tempIterator.next();
            if (reading > 90) {
                System.out.println("Removing heatwave reading: " + reading);
                tempIterator.remove(); // iterator stays in sync — no exception
            }
        }
        System.out.println("Safe temperatures remaining: " + temperatures);

        // Adding elements during iteration — Iterator has no add(), so use collect-then-apply
        System.out.println("\n=== Adding Elements: Collect-Then-Apply Pattern ===");
        List<Integer> extraReadings = new ArrayList<>();
        Iterator<Integer> scanIterator = temperatures.iterator();
        while (scanIterator.hasNext()) {
            int reading = scanIterator.next();
            // Flag each reading that needs a follow-up measurement added after it
            if (reading < 75) {
                extraReadings.add(reading - 2); // simulated second sensor reading
            }
        }
        temperatures.addAll(extraReadings); // apply additions AFTER iteration is complete
        System.out.println("Final temperature list: " + temperatures);
    }
}
Output
=== BROKEN: This will throw ConcurrentModificationException ===
Caught: ConcurrentModificationException
Reason: for-each hides an Iterator — we modified the list externally
=== FIXED: Use iterator.remove() for safe removal ===
Removing heatwave reading: 91
Removing heatwave reading: 95
Safe temperatures remaining: [72, 85, 68]
=== Adding Elements: Collect-Then-Apply Pattern ===
Final temperature list: [72, 85, 68, 70, 66]
Interview Gold: Why 'Fail-Fast'?
Java's fail-fast behavior is a deliberate design decision. Silent data corruption is far more dangerous than a loud exception — if your iterator silently skipped modified elements, you'd have a bug that's nearly impossible to reproduce. Fail-fast forces you to handle the problem correctly, right now.
Production Insight
We had a production incident where a background job silently skipped 30% of records because the developer used list.remove() inside for-each.
The exception was caught in a try-catch and logged, but the loop continued — causing partial processing.
Lesson: never catch ConcurrentModificationException and continue; rework the iteration to use iterator.remove() instead.
Key Takeaway
ConcurrentModificationException is fail-fast by design — it's your friend, not a bug.
Always route structural changes through the iterator.
For additions, use ListIterator.add() or collect-then-apply; for removals, use iterator.remove().

Performance: Iterator vs Index-Based Loop — When It Matters

Many developers assume a traditional for loop with get(i) is faster than an Iterator because it looks simpler. That assumption is dangerously wrong when the collection is a LinkedList. ArrayList.get(i) is O(1) — direct array access. But LinkedList.get(i) is O(n) because it walks the node chain from the beginning each time. An Iterator (or for-each) over a LinkedList is O(n) total — it follows the internal node pointers sequentially.

So for LinkedList: index-based for loop is O(n^2), Iterator is O(n). For ArrayList: both are O(n), but Iterator has slight overhead from the hasNext/next method calls and the iterator object creation. In practice, unless you're iterating millions of elements in a hot loop, the difference is negligible. Always prefer readability: use for-each or explicit Iterator unless you need the index for something other than traversal.

If you need both the element and its index, use ListIterator.nextIndex() or maintain a separate counter. Don't fall back to index-based for loops on unknown collection types.

PerformanceComparison.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import java.util.*;

public class PerformanceComparison {
    public static void main(String[] args) {
        int size = 100_000;
        List<Integer> arrayList = new ArrayList<>(size);
        List<Integer> linkedList = new LinkedList<>();
        for (int i = 0; i < size; i++) {
            arrayList.add(i);
            linkedList.add(i);
        }

        // Warm up
        for (int i = 0; i < 3; i++) {
            iterateWithIndex(arrayList);
            iterateWithIterator(arrayList);
            iterateWithIndex(linkedList);  // first run may include class loading
        }

        long start, end;

        // ArrayList index-based
        start = System.nanoTime();
        iterateWithIndex(arrayList);
        end = System.nanoTime();
        System.out.println("ArrayList index-loop: " + (end - start) / 1_000_000 + " ms");

        // ArrayList iterator
        start = System.nanoTime();
        iterateWithIterator(arrayList);
        end = System.nanoTime();
        System.out.println("ArrayList iterator: " + (end - start) / 1_000_000 + " ms");

        // LinkedList index-based — SLOW
        start = System.nanoTime();
        iterateWithIndex(linkedList);
        end = System.nanoTime();
        System.out.println("LinkedList index-loop: " + (end - start) / 1_000_000 + " ms");

        // LinkedList iterator
        start = System.nanoTime();
        iterateWithIterator(linkedList);
        end = System.nanoTime();
        System.out.println("LinkedList iterator: " + (end - start) / 1_000_000 + " ms");
    }

    static long sum = 0;  // prevent optimisation

    static void iterateWithIndex(List<Integer> list) {
        sum = 0;
        for (int i = 0; i < list.size(); i++) {
            sum += list.get(i);
        }
    }

    static void iterateWithIterator(List<Integer> list) {
        sum = 0;
        for (int val : list) {
            sum += val;
        }
    }
}
Output
ArrayList index-loop: ~2 ms
ArrayList iterator: ~3 ms
LinkedList index-loop: ~4500 ms
LinkedList iterator: ~4 ms
Mental Model: Iterator = Sequential Pointer Walk
  • For an array-based list (ArrayList), an index is just pointer arithmetic — O(1) per access.
  • For a linked list, get(i) starts from the head each time — O(n) per access, O(n²) total.
  • Iterator stores its current position as a node reference — each next() is O(1).
  • Moral: if you don't know the collection type, use iterator/for-each. It's never worse than O(n), and potentially much better.
Production Insight
A microservice that built a report by iterating a LinkedList with index-based for loop caused 30-second request timeouts.
After switching to for-each (iterator), the same report completed in 50ms.
The collection had been changed from ArrayList to LinkedList for thread-safety reasons — but the iteration code wasn't updated.
Key Takeaway
Index-based for loops are O(n^2) on LinkedList.
Iterator/for-each is always O(n).
If you don't control the collection type, always use iterator-based traversal.

ListIterator.add() — Inserting Elements During Iteration

ListIterator.add() is one of the most powerful but most misunderstood methods. It inserts a new element into the list immediately before the current cursor position. If the iterator is in forward iteration (after next()), the cursor is between the element just returned and the next one — add() inserts before the cursor, so it goes after the element you just read. If the iterator is in reverse iteration (after previous()), add() inserts before the cursor, which is after the element you just read backward. The key: add() always inserts before the cursor.

This is perfect for patterns like inserting audit log entries into a time-ordered list while traversing. You walk through the list, and when you encounter a condition, you insert a new entry right before the next element (i.e., after the current one).

AuditLogInsertion.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

public class AuditLogInsertion {

    public static void main(String[] args) {
        List<String> logs = new ArrayList<>();
        logs.add("2026-05-01 10:00: User login");
        logs.add("2026-05-01 10:05: Order placed #1234");
        logs.add("2026-05-01 10:07: Payment processed");
        logs.add("2026-05-01 10:10: User logout");

        ListIterator<String> it = logs.listIterator();

        while (it.hasNext()) {
            String logEntry = it.next();
            if (logEntry.contains("Order placed")) {
                // Insert an audit log entry right after the order placed event
                // Cursor is now between "Order placed" and "Payment processed"
                it.add("2026-05-01 10:06: Order validation started"); // inserts BEFORE cursor -> after current element
                // After add(), the cursor is between the new entry and "Payment processed"
                // Next next() returns "Payment processed"
            }
        }

        System.out.println("=== Audit Log with Insertions ===");
        logs.forEach(System.out::println);
    }
}
Output
=== Audit Log with Insertions ===
2026-05-01 10:00: User login
2026-05-01 10:05: Order placed #1234
2026-05-01 10:06: Order validation started
2026-05-01 10:07: Payment processed
2026-05-01 10:10: User logout
Important: add() Invalidates remove()
After calling add(), you cannot call remove() until you call next() or previous() again. The last operation before remove() must be next/previous, not add. Similarly, after remove(), you cannot call set() until the next next/previous.
Production Insight
We built a real-time order processing system that inserts status updates into an order event list using ListIterator.add().
The trick was to ensure the add() was called during forward iteration, right after next(), so the new entry appeared after the current event.
Saving the cursor position via nextIndex() allowed resuming after crash — exactly-once insertion.
Key Takeaway
ListIterator.add() inserts BEFORE the cursor.
During forward iteration, that means AFTER the current element.
Never call add() then remove() without a next/previous in between.
● Production incidentPOST-MORTEMseverity: high

Production Queue Removal Caused Silent Data Loss

Symptom
Some expired tasks remained in the queue after processing. Users saw stale data. The bug was intermittent — triggered only when two consecutive expired tasks appeared.
Assumption
The developer assumed for-each loop was safe for removal because they had seen 'remove if condition' patterns in blog posts, but those posts always used iterator.remove().
Root cause
Using list.remove() inside a for-each loop (which compiles to Iterator.next()) invalidates the iterator's modCount check. When consecutive elements were removed, the next next() call threw ConcurrentModificationException. When non-consecutive, the list shifted and the iterator skipped the next element — silent data loss.
Fix
Switch to explicit Iterator and call iterator.remove(). Or use ListIterator.remove() for Lists. The collect-then-apply pattern also works: accumulate indices/objects to remove, then remove them after iteration completes.
Key lesson
  • Never call list.remove() or list.add() while iterating with for-each or explicit Iterator. Always route structural changes through the iterator.
  • Silent data corruption is worse than a loud exception — Java's fail-fast design saves you from production bugs that are nearly impossible to reproduce.
  • For bulk removal, use Collection.removeIf() — it's implemented efficiently and handles the Iterator internally.
Production debug guideSymptom → Action — diagnose iterator-related production issues fast.4 entries
Symptom · 01
ConcurrentModificationException thrown during iteration over ArrayList
Fix
Check your loop: are you calling list.remove(), list.add(), or list.clear() inside the loop? If yes, switch to iterator.remove() or use ListIterator for modifications. If using for-each, convert to explicit while(iterator.hasNext()) loop.
Symptom · 02
IllegalStateException when calling Iterator.remove()
Fix
You called remove() without a preceding next() call, or called remove() twice in a row. Ensure the pattern: iterator.next() then iterator.remove(). Each remove() consumes exactly one next() call.
Symptom · 03
ListIterator.set() throws IllegalStateException
Fix
set() must be called after next() or previous(), not after add() or remove(). Check that you haven't performed an add/remove since the last next/previous call.
Symptom · 04
Iterator over a LinkedList is very slow compared to ArrayList
Fix
That's expected — but if you are using an index-based for-loop with LinkedList.get(i), that's O(n^2). Switch to iterator or for-each for O(n) traversal.
★ Iterator + ListIterator Quick Debug Cheat SheetCommon iterator failures and the exact commands or code fixes to apply.
ConcurrentModificationException during for-each
Immediate action
Stop the loop. Identify if the collection is being modified inside the loop body.
Commands
Replace for-each with explicit Iterator: Iterator<T> it = collection.iterator(); while(it.hasNext()) { T item = it.next(); /* modifications via it */ }
For removals only: use collection.removeIf(predicate) — it handles Iterator internally.
Fix now
Change all list.remove() calls to iterator.remove() within the while loop.
ListIterator.add() inserts in wrong position+
Immediate action
Check your mental model: ListIterator.add() inserts BEFORE the cursor (i.e., before the element that next() would return, or after the element that previous() would return).
Commands
Verify cursor position by printing nextIndex() and previousIndex() before the add.
If you want to add after the current element, call next() first (which advances past it), then add().
Fix now
Use add() only in forward iteration after next() to insert after the just-read element.
Iterator.remove() throws IllegalStateException+
Immediate action
You called remove() without a preceding next() call, or twice in a row.
Commands
Check loop structure: ensure next() is called exactly once before each remove().
If you need to remove multiple elements conditionally, use while(iterator.hasNext()) { iterator.next(); if(cond) iterator.remove(); }
Fix now
Restructure: always call next() first, then remove() exactly once per condition.
Iterator vs ListIterator — Feature Comparison
Feature / AspectIteratorListIterator
Works withAny Collection (Set, List, Queue)List only (ArrayList, LinkedList)
DirectionForward onlyForward and backward
MethodshasNext(), next(), remove()hasNext(), next(), hasPrevious(), previous(), nextIndex(), previousIndex(), add(), set(), remove()
Can remove elementsYes, via remove()Yes, via remove()
Can replace elementsNoYes, via set()
Can add elementsNoYes, via add()
Start positionAlways at beginningBeginning or any index via listIterator(index)
Positional awarenessNo — blind to indexYes — nextIndex() and previousIndex()
Performance on LinkedListO(n) total traversalO(n) total traversal
Use caseSafe traversal and removalBidirectional traversal, in-place editing, undo/redo patterns
Available sinceJava 1.2Java 1.2

Key takeaways

1
Iterator is the engine under every for-each loop
understanding it explicitly gives you the remove() power that for-each deliberately hides from you.
2
ListIterator is Iterator's supercharged sibling
bidirectional traversal, set(), add(), and index awareness — but it only works on Lists, not Sets or Queues.
3
ConcurrentModificationException is fail-fast by design
always route structural changes through the iterator (iterator.remove(), listIterator.set()) or use the collect-then-apply pattern for additions.
4
The ListIterator cursor sits between elements, not on them
calling next() then previous() returns the same element twice, which is correct behavior and is the foundation of in-place editing patterns.
5
Performance trap
index-based for loops on LinkedList are O(n^2) — always use Iterator or for-each when the collection type is unknown.
6
ListIterator.add() inserts BEFORE the cursor
during forward iteration, that's AFTER the last element returned by next().

Common mistakes to avoid

4 patterns
×

Calling list.remove() inside a for-each loop

Symptom
Either ConcurrentModificationException is thrown (if consecutive elements are removed) or elements are silently skipped (if non-consecutive). The loop may complete with partial removals.
Fix
Convert to explicit Iterator and call iterator.remove(). Or use ListIterator for replacements. For bulk removal, prefer collection.removeIf(predicate).
×

Calling iterator.remove() without calling next() first

Symptom
IllegalStateException thrown on the remove() call. The iterator has no current element to remove because next() hasn't been called or remove() was called twice consecutively.
Fix
Always call next() exactly once before each remove(). The pattern is: iterator.next(); if(condition) iterator.remove();.
×

Assuming Iterator works on Sets in a predictable order

Symptom
Logic that depends on iteration order silently produces wrong results. For example, processing elements of a HashSet in insertion order fails intermittently.
Fix
Use LinkedHashSet for insertion-order iteration, or TreeSet for sorted order. Document ordering assumptions clearly. For HashMap, use LinkedHashMap if ordering matters.
×

Using index-based for loop on a LinkedList

Symptom
Severe performance degradation — O(n^2) time instead of O(n). The application may appear to hang or timeout for large lists.
Fix
Always use Iterator (or for-each) for traversing LinkedList. If you need the index, use ListIterator.nextIndex() or maintain a separate counter.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between Iterator and ListIterator in Java, and wh...
Q02SENIOR
Why does ConcurrentModificationException get thrown when you modify a Li...
Q03SENIOR
What is the difference between Iterator.remove() and List.remove(), and ...
Q04SENIOR
Explain the cursor model of ListIterator. What happens if you call add()...
Q01 of 04JUNIOR

What is the difference between Iterator and ListIterator in Java, and when would you choose one over the other?

ANSWER
Iterator is a universal interface for forward-only traversal and safe removal over any Collection. ListIterator extends Iterator and is exclusive to List implementations. It adds backward traversal (hasPrevious(), previous()), element replacement (set()), addition (add()), and index awareness (nextIndex(), previousIndex()). Use Iterator when you only need forward traversal and removal. Use ListIterator when you need bidirectional traversal, in-place editing, or insertion during iteration.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Can I use Iterator with a Set or Map in Java?
02
Is Iterator faster than a for-each loop in Java?
03
What happens if I call listIterator.set() without calling next() or previous() first?
04
Can I have multiple iterators over the same collection simultaneously?
05
What's the difference between fail-fast and fail-safe iterators?
🔥

That's Collections. Mark it forged?

5 min read · try the examples if you haven't

Previous
Stack and Queue in Java
8 / 21 · Collections
Next
LinkedHashMap and LinkedHashSet