skip to content

What does ListIterator add over the basic Iterator, and when would you reach for it?

level: middleimportance: should knowfreq 45%

answer

  1. ListIterator extends Iterator, Lists only
  2. Bidirectional: hasPrevious/previous
  3. set() replaces (non-structural), add() inserts
  4. nextIndex/previousIndex for position
  5. Cursor sits BETWEEN elements (next then previous = same element)

basics

~20 s

ListIterator is a more powerful cursor for lists. On top of hasNext()/next(), it can go backwards (hasPrevious()/previous()), report indices, replace the current element with set(), and insert with add(). You get it from a List via listIterator().

solid answer

~40 s

ListIterator<E> extends Iterator<E> and is available only on Lists (via list.listIterator() or listIterator(index)). It adds bidirectional traversal — hasPrevious()/previous() — plus nextIndex()/previousIndex() to know your position, set(e) to replace the element returned by the last next()/previous() without a structural change, and add(e) to insert at the cursor (structural, keeps the iterator valid). Reach for it when you must: walk a list backwards, replace elements in place during a single pass (set), or insert while traversing (add) — all things a plain Iterator cannot do, since Iterator only offers forward movement and remove(). A subtle point: next() and previous() are not symmetric mirror images — after next() returns element i, previous() returns that same element i again, because the cursor sits between elements.

code

java · 13 lines
java
List<String> words = new ArrayList<>(List.of("a", "bb", "ccc"));
ListIterator<String> li = words.listIterator();
while (li.hasNext()) {
    String w = li.next();
    li.set(w.toUpperCase());        // in-place replace, no CME
    if (w.length() == 2) li.add("!"); // insert via iterator, stays valid
}
// words is now [A, BB, !, CCC]

// backward walk
while (li.hasPrevious()) {
    System.out.println(li.previousIndex() + ": " + li.previous());
}

go deeper

for a junior

Knows ListIterator can go backwards and replace elements, and is obtained from a List.

for a middle

Can use set/add/remove correctly, explains the between-elements cursor model and the next-then-previous returns-same-element subtlety.

for a senior

Knows set() is non-structural (no CME) while add() goes through the iterator to stay valid, and that ListIterator is the efficient traversal for LinkedList.

for a principal

Weighs ListIterator vs streams vs index loops for in-place transformation and insertion patterns, and reasons about API ergonomics and performance per list implementation.

## Where ListIterator fits `Iterator<E>` is the minimal forward-only cursor: `hasNext()`, `next()`, and an optional `remove()`. `ListIterator<E>` is a **sub-interface** (`extends Iterator<E>`) available specifically for the `List` family, obtained via `list.listIterator()` or `list.listIterator(startIndex)`. Because lists are *ordered and indexed*, the cursor can do more than a generic collection cursor. ## The cursor-between-elements model The key mental model: a `ListIterator`'s position is **between** two elements, like a text caret. For a list `[A, B, C]` the cursor can sit at positions 0..3: ``` 0 1 2 3 A B C ``` - `nextIndex()` returns the index that `next()` would return (the element just to the right). - `previousIndex()` returns the index `previous()` would return (the element just to the left); it is `nextIndex() - 1`. - `next()` returns the element to the right and moves the caret right; `previous()` returns the element to the left and moves left. This explains the **non-symmetry**: if you call `next()` and get `B`, then immediately call `previous()`, you get `B` again — the caret merely moved back across the same element it just crossed. They are not inverse 'step by one element' operations on distinct elements. ## The added methods 1. **`hasPrevious()` / `previous()`** — backward traversal. Plain `Iterator` has no way to go back. 2. **`nextIndex()` / `previousIndex()`** — positional awareness, useful when you need the index alongside the element. 3. **`set(E e)`** — replaces the element last returned by `next()` or `previous()`. This is an **in-place replacement**, not a structural change, so it does **not** bump `modCount` and never causes a CME. It is the clean way to transform a list during a single pass. 4. **`add(E e)`** — inserts `e` immediately before the element `next()` would return (at the caret). This **is** structural, but performed through the iterator it keeps `modCount`/`expectedModCount` in sync, so the iterator stays valid (unlike calling `list.add` directly). It also inherits `remove()` from `Iterator`, with the rule that `set`/`remove` operate on the **last** element returned and cannot be called immediately after an `add` or after a `remove` without an intervening `next`/`previous`. ## When to reach for it - **Replacing elements in place during traversal** — e.g. normalising every string in a list. With a plain for-each you cannot write back; `ListIterator.set()` does it cleanly in one pass. - **Inserting while iterating** — building or expanding a list mid-walk (e.g. inserting separators) via `add()`. - **Backward iteration** — scanning a list from the end, or two-way scanning. - **Needing the index** alongside the value without a manual counter. If you only read forward, the simpler `Iterator`/for-each is preferable; `ListIterator` is the tool when you need positional or mutating control over an ordered list. ## Performance note On an `ArrayList`, `set`/`get`-style access is O(1). On a `LinkedList`, a `ListIterator` is actually the *efficient* way to walk and mutate, because it holds a node pointer — index-based `get(i)` on a `LinkedList` is O(n) per call, but stepping the iterator is O(1).

  • If you call next() then previous() without changing anything, what does previous() return?
    The same element next() just returned. The cursor lives between elements, so previous() steps the caret back across the element it just crossed.
  • Why is set() safe during iteration while list.set(i, x) outside the iterator is also fine, but list.add(x) is not?
    set() (either form) only replaces a value and is non-structural, so modCount is unchanged. list.add() is structural and, done outside the iterator, desyncs modCount and triggers CME; ListIterator.add() does it through the iterator and re-syncs the counter.

saying these in an interview costs you the question

  • Thinking ListIterator works on any Collection — it is List-only
  • Believing set() is structural / can cause CME
  • Assuming next() and previous() always move to different elements
  • Confusing ListIterator.add() (valid, in-sync) with list.add() during iteration (CME)

context