Java Iterator/ListIterator — Silent Data Loss from remove()
Silent data loss: list.
- 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.
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.
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.list.remove() inside a for-each loop.iterator.remove() or collect-then-apply.Iterator.remove() keeps the cursor and collection in sync.remove() access.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.
set(), add(), and index awareness.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.
list.remove() inside for-each.iterator.remove() instead.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.
- 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.
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).
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.ListIterator.add().add() was called during forward iteration, right after next(), so the new entry appeared after the current event.ListIterator.add() inserts BEFORE the cursor.add() then remove() without a next/previous in between.Production Queue Removal Caused Silent Data Loss
iterator.remove().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.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.- Never call
list.remove()orlist.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.
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.Iterator.remove()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.ListIterator.set() throws IllegalStateExceptionnext() or previous(), not after add() or remove(). Check that you haven't performed an add/remove since the last next/previous call.list.remove() calls to iterator.remove() within the while loop.Key takeaways
remove() power that for-each deliberately hides from you.set(), add(), and index awareness — but it only works on Lists, not Sets or Queues.iterator.remove(), listIterator.set()) or use the collect-then-apply pattern for additions.next() then previous() returns the same element twice, which is correct behavior and is the foundation of in-place editing patterns.ListIterator.add() inserts BEFORE the cursornext().Common mistakes to avoid
4 patternsCalling list.remove() inside a for-each loop
iterator.remove(). Or use ListIterator for replacements. For bulk removal, prefer collection.removeIf(predicate).Calling iterator.remove() without calling next() first
remove() call. The iterator has no current element to remove because next() hasn't been called or remove() was called twice consecutively.next() exactly once before each remove(). The pattern is: iterator.next(); if(condition) iterator.remove();.Assuming Iterator works on Sets in a predictable order
Using index-based for loop on a LinkedList
ListIterator.nextIndex() or maintain a separate counter.Interview Questions on This Topic
What is the difference between Iterator and ListIterator in Java, and when would you choose one over the other?
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.Frequently Asked Questions
That's Collections. Mark it forged?
5 min read · try the examples if you haven't