📦 Section 4 · Question #25

What is the difference between Iterator and ListIterator

Both Iterator and ListIterator are standard interfaces in java.util designed for sequential collection traversal. However, ListIterator is a dedicated sub-interface that extends...


🟢 Junior Level

1. Core Differences

Both Iterator and ListIterator are standard interfaces in java.util designed for sequential collection traversal. However, ListIterator is a dedicated sub-interface that extends Iterator (public interface ListIterator<E> extends Iterator<E>), engineered specifically for indexed lists (List):

Characteristic Iterator ListIterator
Applicability Any Collection (List, Set, Queue) List implementations only
Traversal Direction Forward only (next()) Bidirectional (next() and previous())
Index Tracking None nextIndex() and previousIndex()
Element Modification Deletion only: remove() Full mutation: remove(), set(e), add(e)
Starting Position Always starts at index 0 (Head) Can initialize at any arbitrary index (listIterator(index))
List<String> list = new ArrayList<>(List.of("A", "B", "C"));

// 1. Standard Iterator: Forward traversal only
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    System.out.print(it.next() + " "); // Prints: A B C 
}

// 2. ListIterator: Reverse traversal from tail to head
ListIterator<String> listIt = list.listIterator(list.size()); // Cursor starts at the end
while (listIt.hasPrevious()) {
    System.out.print(listIt.previous() + " "); // Prints: C B A 
}

🟡 Middle Level

1. The Virtual Cursor Model

To understand the behavior of ListIterator methods, it is crucial to recognize that the cursor logically sits between elements, rather than pointing directly at an element:

Index:            0             1             2
Elements:    [ "Alpha" ]   [ "Beta" ]   [ "Gamma" ]
Cursor:    ▲             ▲            ▲             ▲
Position:  0             1            2             3
  • next(): Returns the element to the right of the cursor and advances the cursor one position to the right.
  • previous(): Returns the element to the left of the cursor and moves the cursor one position to the left.
  • nextIndex(): Returns the index of the element that would be returned by a subsequent call to next().
  • previousIndex(): Returns the index of the element that would be returned by a subsequent call to previous().

2. Advanced Mutations: set() and add()

Unlike Iterator, which only permits deleting the current item, ListIterator supports in-place replacement and insertion:

List<String> names = new ArrayList<>(List.of("Ivan", "Peter", "Anna"));
ListIterator<String> it = names.listIterator();

// 1. set(E e) - Replaces the LAST RETURNED element:
while (it.hasNext()) {
    if (it.next().equals("Peter")) {
        it.set("Peter the Great"); // Replaces "Peter" with "Peter the Great"
    }
}

// 2. add(E e) - Inserts an element immediately BEFORE the cursor:
it = names.listIterator();
while (it.hasNext()) {
    if (it.next().equals("Ivan")) {
        it.add("Sergey"); // "Sergey" is inserted between "Ivan" and "Peter the Great"
    }
}
System.out.println(names); // [Ivan, Sergey, Peter the Great, Anna]

🔴 Senior Level

1. The State Contract: When IllegalStateException Occurs

The remove() and set(e) operations in ListIterator are tied to the concept of the Last Returned Element:

  • Calling set(e) or remove() is valid only after an explicit call to next() or previous().
  • Calling add(e) clears the reference to the last returned element.
  • Calling remove() clears the reference to the last returned element.
ListIterator<String> it = list.listIterator();
it.next(); // Returns element at index 0

it.add("New"); // Element inserted before cursor; lastRet reset to null!
it.set("Replacement"); // ❌ DISASTER: Throws java.lang.IllegalStateException!

it.remove(); // ❌ DISASTER: Throws java.lang.IllegalStateException!

Why this fails: add() advances the cursor past the newly inserted element. The iterator cannot determine which element set() is intended to replace: the element returned by the previous next(), or the element just inserted. To prevent ambiguity and race conditions, the JDK specification strictly forbids calling set() or remove() immediately following add().


2. Rescuing LinkedList from $O(N^2)$ Traversal Catastrophe

A classic performance anti-pattern is traversing a LinkedList via an indexed for loop:

// ❌ CATASTROPHIC ANTI-PATTERN: O(N^2) Quadratic Traversal!
for (int i = 0; i < linkedList.size(); i++) {
    String item = linkedList.get(i); // get(i) traverses pointers from head/tail on every single call!
}

// ✅ OPTIMAL: Strictly O(N) Linear Traversal via ListIterator
ListIterator<String> it = linkedList.listIterator();
while (it.hasNext()) {
    String item = it.next(); // O(1) step: follows active node.next reference!
    if (shouldInsert) {
        it.add("newItem");   // O(1) node unlinking/insertion at current position!
    }
}

Internally, LinkedList.ListItr maintains a direct reference to the current Node<E> next. Traversals and structural modifications execute in $O(1)$ pointer operations, transforming an $O(N^2)$ algorithm into a linear $O(N)$ pipeline.


4 Tricky Questions

1. Exactly where is an element inserted when calling ListIterator.add(E e), and what will subsequent calls to next() and previous() return?

Answer: The element is inserted immediately before the virtual cursor position. Consequently:

  • A subsequent call to it.next() is unaffected: it returns the same element to the right of the cursor that it would have returned prior to the insertion.
  • A subsequent call to it.previous() returns the newly inserted element.
  • The value returned by nextIndex() increments by 1.

2. Why does the Set interface not provide a setIterator() with bidirectional traversal?

Answer: By mathematical and algorithmic definition, a Set (such as HashSet) models an unordered collection of elements distributed across hash buckets. There is no concept of positional indexing, no stable concept of “previous neighbor,” and no guaranteed order. Even sorted or sequenced sets (TreeSet, LinkedHashSet) operate on key order or insertion chronology rather than contiguous integer indices, making index tracking (nextIndex, previousIndex) meaningless.


3. Can you invoke it.remove() twice consecutively after a single call to it.next()?

Answer: No. The second consecutive call will immediately throw an IllegalStateException. remove() deletes the last returned element and sets the internal tracking pointer lastRet to null. Attempting to call remove() again before advancing the cursor with another next() or previous() fails because there is no currently referenced element to remove.


4. Why does ListIterator.set(e) throw IllegalStateException if invoked immediately after ListIterator.add(e)?

Answer: The contract for set(e) stipulates that it replaces the element returned by the most recent call to next() or previous(). Calling add(e) inserts a new element before the cursor and resets the internal lastRet reference to null. Because add() shifts cursor coordinates, it is ambiguous whether set() should replace the item returned prior to add() or the newly added item. The JDK eliminates this ambiguity by disallowing set() until another navigational step (next()/previous()) is executed.


🎯 Interview Cheat Sheet

30-Second Elevator Pitch

“Iterator is the universal interface for one-way forward traversal across any collection (List, Set, Queue), supporting only the remove() mutation. ListIterator is a specialized extension available strictly for Lists. It provides bidirectional traversal (hasPrevious(), previous()), tracks index positions (nextIndex(), previousIndex()), allows starting at arbitrary indices (listIterator(index)), and supports both in-place replacement (set(e)) and insertion (add(e)). For LinkedList, using ListIterator is critical: it reduces indexed traversals from catastrophic $O(N^2)$ to $O(N)$ linear time.”

High-Impact Architectural Facts

  • Cursor Abstraction: The cursor sits between elements; add(e) inserts before the cursor, while set(e) mutates the last returned element.
  • State Invalidation: Calling add() or remove() resets the lastRet pointer to null, causing subsequent set() calls to throw IllegalStateException.
  • LinkedList Optimization: ListIterator holds direct pointers to Node<E>, eliminating repeated head-to-index pointer chases.
  • Fail-Fast Invariant: Modifying the backing list concurrently while using ListIterator triggers ConcurrentModificationException.

Common Pitfalls & Red Flags

  • ❌ Calling listIterator() on a Set or Queue: The method exists strictly on the List interface.
  • ❌ Invoking set() immediately after add(): Triggers an IllegalStateException.
  • ❌ Using list.get(i) in a loop over LinkedList: Collapses performance to $O(N^2)$; always use ListIterator.
  • ❌ Assuming ListIterator is thread-safe: It is fail-fast and requires external synchronization in multi-threaded environments.