Java Tutorial
🔍

Java ListIterator

Java ListIterator

java.util.ListIterator<E> is the extension of Iterator that gives you full bidirectional control over a List. Where a plain Iterator can only move forward and remove, a ListIterator can move backward, replace elements in place, insert new elements mid-traversal, and report the current cursor position as an index. It is the only standard Java mechanism for all of these in a single object.

If you have ever needed to scan a list forward then process it backward, or replace every element during iteration without manually tracking indices, ListIterator is the right tool.

What Is ListIterator?

ListIterator<E> is an interface in java.util that extends java.util.Iterator<E>. It adds five new methods to the three that Iterator provides:

java.util.Iterator<E>                   ← base traversal
    boolean  hasNext()                  ← more elements forward?
    E        next()                     ← return current, advance forward
    void     remove()                   ← remove last returned element

java.util.ListIterator<E>               ← extends Iterator — List only
    boolean  hasPrevious()              ← more elements backward?
    E        previous()                 ← return current, advance backward
    int      nextIndex()                ← index of element next() would return
    int      previousIndex()            ← index of element previous() would return
    void     set(E element)             ← replace last returned element (no cursor shift)
    void     add(E element)             ← insert before next-to-be-returned element

The diagram below shows the cursor model. ListIterator positions a conceptual cursor between elements, not on them.

LIST CONTENTS:    [ "A"  |  "B"  |  "C"  |  "D"  |  "E" ]
                   0       1       2       3       4      (element indices)

CURSOR POSITIONS:
  ^              ^       ^       ^       ^       ^
  0              1       2       3       4       5        (cursor positions)

  nextIndex()    = index of next element next() would return
  previousIndex() = nextIndex() - 1

  Cursor at position 2:
    nextIndex()     = 2  → next() returns "C"
    previousIndex() = 1  → previous() returns "B"

  After next() when cursor is at position 2:
    cursor moves to position 3
    next() returns "C"    (element at old cursor 2)
    lastRet = 2           (remembers what was returned)

  After previous() when cursor is at position 3:
    cursor moves back to position 2
    previous() returns "C"  (element at old cursor 2)
    lastRet = 2

ListIterator is only available on List implementations. Calling list.listIterator() gives a cursor at position 0. Calling list.listIterator(int index) starts the cursor at any valid position from 0 to list.size().

Where ListIterator Fits in the Hierarchy

java.lang.Iterable<E>
    └── java.util.Collection<E>
            └── java.util.List<E>
                    ├── ArrayList   ← supports listIterator()
                    ├── LinkedList  ← supports listIterator()
                    └── Vector      ← supports listIterator() (legacy)

java.util.Iterator<E>              ← base interface
    └── java.util.ListIterator<E>  ← List-specific extension

  Note: Set, Queue, Map views do NOT support ListIterator.
  Only List implementations have a listIterator() method.

When to Use ListIterator

The decision is direct. Use ListIterator when all of these are true: the collection is a List, and at least one of the following operations is needed during traversal:

  • Backward traversal — scanning a list in reverse without reversing the list itself
  • In-place replacementset(newElement) replaces an element without a separate list.set(index, value) call
  • Mid-traversal insertionadd(newElement) inserts before the next element without index bookkeeping
  • Current position awarenessnextIndex() and previousIndex() tell you exactly where the cursor is

Use plain Iterator when only forward traversal with optional removal is needed. Use for-each when traversal is read-only. Use removeIf() for conditional bulk removal. Use a standard index loop when random access by position is required alongside traversal.

How ListIterator Works Internally

ListIterator for ArrayList is implemented as an inner class called ListItr that extends Itr (the regular iterator inner class). It adds three fields beyond the base cursor:

ARRAYLIST'S ListItr INTERNAL FIELDS:

  From Itr (parent):
    int cursor           ← index of next element that next() will return
    int lastRet = -1     ← index of last element returned by next() or previous()
    int expectedModCount ← snapshot of modCount at iterator creation

  Added by ListItr:
    (no new fields — ListItr uses cursor and lastRet differently)

  CURSOR SEMANTICS:
    next():
      1. Check modCount == expectedModCount → CME if changed outside iterator
      2. result = elementData[cursor]
      3. lastRet = cursor
      4. cursor++
      5. return result

    previous():
      1. Check modCount == expectedModCount → CME if changed outside iterator
      2. cursor--
      3. result = elementData[cursor]
      4. lastRet = cursor
      5. return result

    set(element):
      1. Check lastRet >= 0 → IllegalStateException if no prior next/previous
      2. Check modCount == expectedModCount → CME if stale
      3. ArrayList.this.set(lastRet, element)   ← no cursor change
      4. (modCount NOT incremented — set() is not structural)

    add(element):
      1. Check modCount == expectedModCount → CME if stale
      2. ArrayList.this.add(cursor, element)   ← inserts at current position
      3. cursor++                              ← skip the added element on next next()
      4. lastRet = -1                          ← prevents set/remove after add
      5. expectedModCount = modCount           ← sync after structural change

KEY INSIGHT:
  set() is NOT structural — it does not change size or modCount.
  add() IS structural — it updates expectedModCount after the change.
  remove() IS structural — it updates expectedModCount after the change.
  This is why set() after add() or after remove() throws IllegalStateException —
  lastRet is reset to -1 by both add() and remove(), making the "last returned"
  position undefined.

Core Operations with Examples

Forward and Backward Traversal

The foundational ListIterator capability — scanning in both directions. The cursor position persists between hasNext()/next() and hasPrevious()/previous() calls, so you can switch direction at any point.

1// File: ListIteratorTraversalDemo.java 2 3import java.util.ArrayList; 4import java.util.List; 5import java.util.ListIterator; 6 7public class ListIteratorTraversalDemo { 8 9 public static void main(String[] args) { 10 11 List<String> stations = new ArrayList<>( 12 List.of("Churchgate", "Marine Lines", "Charni Road", 13 "Grant Road", "Mumbai Central", "Mahalaxmi") 14 ); 15 16 // Forward traversal with index reporting 17 System.out.println("=== Forward pass ==="); 18 ListIterator<String> lit = stations.listIterator(); 19 while (lit.hasNext()) { 20 int idx = lit.nextIndex(); 21 String station = lit.next(); 22 System.out.printf(" [%d] %s%n", idx, station); 23 } 24 25 // After forward pass, cursor is at the end — switch to backward 26 System.out.println("\n=== Backward pass (cursor at end after forward) ==="); 27 while (lit.hasPrevious()) { 28 int idx = lit.previousIndex(); 29 String station = lit.previous(); 30 System.out.printf(" [%d] %s%n", idx, station); 31 } 32 33 // Start traversal from a specific index 34 System.out.println("\n=== Traversal starting at index 3 ==="); 35 ListIterator<String> midIt = stations.listIterator(3); 36 System.out.println(" nextIndex() = " + midIt.nextIndex()); 37 System.out.println(" previousIndex() = " + midIt.previousIndex()); 38 while (midIt.hasNext()) { 39 System.out.println(" " + midIt.next()); 40 } 41 } 42}
Output:
=== Forward pass ===
  [0] Churchgate
  [1] Marine Lines
  [2] Charni Road
  [3] Grant Road
  [4] Mumbai Central
  [5] Mahalaxmi

=== Backward pass (cursor at end after forward) ===
  [5] Mahalaxmi
  [4] Mumbai Central
  [3] Grant Road
  [2] Charni Road
  [1] Marine Lines
  [0] Churchgate

=== Traversal starting at index 3 ===
  nextIndex()     = 3
  previousIndex() = 2
  Grant Road
  Mumbai Central
  Mahalaxmi

In-Place Replacement with set()

set(element) replaces the element most recently returned by next() or previous() without moving the cursor. This is the correct way to transform every element in a list during traversal — cleaner and more efficient than tracking an index manually.

1// File: ListIteratorSetDemo.java 2 3import java.util.ArrayList; 4import java.util.List; 5import java.util.ListIterator; 6 7public class ListIteratorSetDemo { 8 9 public static void main(String[] args) { 10 11 List<String> productNames = new ArrayList<>( 12 List.of("laptop", "wireless mouse", "mechanical keyboard", 13 "monitor", "usb hub", "webcam") 14 ); 15 System.out.println("Before: " + productNames); 16 17 // Normalise: capitalise first letter of each product name 18 // set() replaces the last element returned — cursor does not move 19 ListIterator<String> lit = productNames.listIterator(); 20 while (lit.hasNext()) { 21 String name = lit.next(); 22 String capitalised = name.substring(0, 1).toUpperCase() + name.substring(1); 23 lit.set(capitalised); // replaces "laptop" with "Laptop" etc. 24 } 25 System.out.println("After capitalise: " + productNames); 26 27 // Apply a price suffix during a backward pass 28 List<String> items = new ArrayList<>( 29 List.of("Tata Salt", "Amul Butter", "Aashirvaad Atta", "Fortune Oil") 30 ); 31 ListIterator<String> backIt = items.listIterator(items.size()); // start at end 32 while (backIt.hasPrevious()) { 33 String item = backIt.previous(); 34 backIt.set(item + " [OFFER]"); // replace while traversing backward 35 } 36 System.out.println("\nAfter backward set(): " + items); 37 38 // set() cannot follow add() or remove() — demonstrates IllegalStateException 39 List<String> demo = new ArrayList<>(List.of("X", "Y", "Z")); 40 ListIterator<String> setAfterAdd = demo.listIterator(); 41 setAfterAdd.next(); // returns "X" 42 setAfterAdd.add("INSERTED"); // add resets lastRet to -1 43 try { 44 setAfterAdd.set("REPLACED"); // IllegalStateException — lastRet is -1 45 } catch (IllegalStateException e) { 46 System.out.println("\nset() after add() throws: " + e.getClass().getSimpleName()); 47 } 48 } 49}
Output:
Before: [laptop, wireless mouse, mechanical keyboard, monitor, usb hub, webcam]
After capitalise: [Laptop, Wireless mouse, Mechanical keyboard, Monitor, Usb hub, Webcam]

After backward set(): [Tata Salt [OFFER], Amul Butter [OFFER], Aashirvaad Atta [OFFER], Fortune Oil [OFFER]]

set() after add() throws: IllegalStateException

Mid-Traversal Insertion with add()

add(element) inserts the new element immediately before the element that next() would return next. The cursor advances past the inserted element, so next() after add() skips it and returns the element that was originally next.

1// File: ListIteratorAddDemo.java 2 3import java.util.ArrayList; 4import java.util.List; 5import java.util.ListIterator; 6 7public class ListIteratorAddDemo { 8 9 public static void main(String[] args) { 10 11 // Task queue — insert a priority task before every STANDARD task 12 List<String> taskQueue = new ArrayList<>( 13 List.of("STANDARD: Send report", 14 "STANDARD: Update DB", 15 "URGENT: Fix login bug", 16 "STANDARD: Deploy feature") 17 ); 18 System.out.println("Before: " + taskQueue); 19 20 ListIterator<String> lit = taskQueue.listIterator(); 21 while (lit.hasNext()) { 22 String task = lit.next(); 23 if (task.startsWith("STANDARD")) { 24 // Insert a logging task before each standard task 25 // add() inserts before next() position and advances cursor past it 26 // so the added element is NOT returned again by the same next() call 27 lit.add("LOG: Before - " + task.substring(10)); 28 } 29 } 30 System.out.println("After insertions: "); 31 taskQueue.forEach(t -> System.out.println(" " + t)); 32 33 // nextIndex and previousIndex after add() 34 System.out.println(); 35 List<Integer> nums = new ArrayList<>(List.of(10, 30, 50)); 36 ListIterator<Integer> numIt = nums.listIterator(); 37 System.out.println("nextIndex before next() : " + numIt.nextIndex()); 38 numIt.next(); // returns 10, cursor → 1 39 System.out.println("nextIndex after next() : " + numIt.nextIndex()); 40 numIt.add(20); // inserts 20 at index 1, cursor → 2 41 System.out.println("nextIndex after add(20) : " + numIt.nextIndex()); 42 System.out.println("previousIndex after add(20): " + numIt.previousIndex()); 43 System.out.println("List after add: " + nums); // [10, 20, 30, 50] 44 System.out.println("next() after add returns : " + numIt.next()); // 30 45 } 46}
Output:
Before: [STANDARD: Send report, STANDARD: Update DB, URGENT: Fix login bug, STANDARD: Deploy feature]
After insertions:
  LOG: Before - Send report
  STANDARD: Send report
  LOG: Before - Update DB
  STANDARD: Update DB
  URGENT: Fix login bug
  LOG: Before - Deploy feature
  STANDARD: Deploy feature

nextIndex before next()    : 0
nextIndex after next()     : 1
nextIndex after add(20)    : 2
previousIndex after add(20): 1
List after add: [10, 20, 30, 50]
next() after add returns   : 30

Safe Removal with remove()

remove() on a ListIterator behaves like Iterator.remove() — it removes the last element returned by next() or previous(). It cannot be called consecutively without an intervening next() or previous().

1// File: ListIteratorRemoveDemo.java 2 3import java.util.ArrayList; 4import java.util.List; 5import java.util.ListIterator; 6 7public class ListIteratorRemoveDemo { 8 9 public static void main(String[] args) { 10 11 List<Integer> scores = new ArrayList<>( 12 List.of(95, 42, 88, 15, 76, 33, 91, 60, 8, 100) 13 ); 14 System.out.println("Original scores: " + scores); 15 16 // Forward pass: remove scores below 50 17 ListIterator<Integer> it = scores.listIterator(); 18 while (it.hasNext()) { 19 int score = it.next(); 20 if (score < 50) { 21 it.remove(); // safe — adjusts cursor and syncs modCount 22 } 23 } 24 System.out.println("After removing below 50: " + scores); 25 26 // Backward pass: remove top two scores (scores >= 90) 27 // By traversing backward, we can check highest values first 28 ListIterator<Integer> revIt = scores.listIterator(scores.size()); 29 int removedCount = 0; 30 while (revIt.hasPrevious() && removedCount < 2) { 31 int score = revIt.previous(); 32 if (score >= 90) { 33 revIt.remove(); 34 removedCount++; 35 } 36 } 37 System.out.println("After removing top 2 (>=90): " + scores); 38 39 // Attempting double-remove — demonstrates IllegalStateException 40 List<String> demo = new ArrayList<>(List.of("A", "B", "C")); 41 ListIterator<String> demoIt = demo.listIterator(); 42 demoIt.next(); // returns "A" 43 demoIt.remove(); // removes "A" — lastRet reset to -1 44 try { 45 demoIt.remove(); // throws — no intervening next() since last remove() 46 } catch (IllegalStateException e) { 47 System.out.println("\nDouble remove() throws: " + e.getClass().getSimpleName()); 48 } 49 } 50}
Output:
Original scores: [95, 42, 88, 15, 76, 33, 91, 60, 8, 100]
After removing below 50: [95, 88, 76, 91, 60, 100]
After removing top 2 (>=90): [88, 76, 60]

Double remove() throws: IllegalStateException

ListIterator vs Iterator

FeatureIteratorListIterator
Interfacejava.util.Iteratorjava.util.ListIterator
ExtendsIterator
Works onAny CollectionList only
DirectionForward onlyForward and backward
hasNext()YesYes
next()YesYes
hasPrevious()NoYes
previous()NoYes
nextIndex()NoYes
previousIndex()NoYes
remove()YesYes
set(element)NoYes
add(element)NoYes
Start positionAlways position 0Any valid index

The key practical differences: ListIterator is needed when any of these is required — backward scanning, in-place replacement during traversal, or insertion at the current cursor position. For everything else, Iterator or for-each is sufficient.

Real-World Example — Meesho Product Feed Normaliser

A product feed normaliser at Meesho processes a batch of seller-uploaded product listings. Each listing must be scanned twice: a forward pass to normalise and flag issues, and a backward pass to insert placeholder entries for missing categories. Using ListIterator for both passes avoids rebuilding the list or maintaining separate index counters.

1// File: ProductListing.java 2 3public class ProductListing { 4 5 private String title; 6 private String category; 7 private double price; 8 private boolean hasIssue; 9 10 public ProductListing(String title, String category, double price) { 11 this.title = title; 12 this.category = category; 13 this.price = price; 14 this.hasIssue = false; 15 } 16 17 public String getTitle() { return title; } 18 public String getCategory() { return category; } 19 public double getPrice() { return price; } 20 public boolean hasIssue() { return hasIssue; } 21 22 public void setTitle(String title) { this.title = title; } 23 public void setCategory(String cat) { this.category = cat; } 24 public void setHasIssue(boolean flag) { this.hasIssue = flag; } 25 26 @Override 27 public String toString() { 28 return String.format("%-30s %-12s Rs.%7.2f%s", 29 title, category, price, hasIssue ? " [FLAG]" : ""); 30 } 31}
1// File: FeedNormaliser.java 2 3import java.util.ArrayList; 4import java.util.List; 5import java.util.ListIterator; 6 7public class FeedNormaliser { 8 9 // Forward pass: normalise titles and flag invalid prices 10 static void forwardNormalise(List<ProductListing> listings) { 11 ListIterator<ProductListing> it = listings.listIterator(); 12 while (it.hasNext()) { 13 ProductListing listing = it.next(); 14 15 // Normalise: trim title and capitalise first letter 16 String cleanTitle = listing.getTitle().strip(); 17 if (!cleanTitle.isEmpty()) { 18 cleanTitle = cleanTitle.substring(0, 1).toUpperCase() 19 + cleanTitle.substring(1).toLowerCase(); 20 } 21 listing.setTitle(cleanTitle); 22 23 // Flag listings with suspicious price (below Rs. 1 or above Rs. 500000) 24 if (listing.getPrice() < 1.0 || listing.getPrice() > 500_000.0) { 25 listing.setHasIssue(true); 26 } 27 28 // Replace listing in-place — set() does not shift any elements 29 it.set(listing); 30 } 31 } 32 33 // Backward pass: insert a placeholder BEFORE each flagged listing 34 static void backwardInsertPlaceholders(List<ProductListing> listings) { 35 ListIterator<ProductListing> it = listings.listIterator(listings.size()); 36 while (it.hasPrevious()) { 37 ProductListing listing = it.previous(); 38 if (listing.hasIssue()) { 39 // add() inserts before next-to-be-returned by next() 40 // Since we called previous(), cursor is now before this listing 41 // add() here inserts before this listing in forward order 42 it.add(new ProductListing( 43 "REVIEW REQUIRED", listing.getCategory(), 0.0)); 44 } 45 } 46 } 47 48 static void printFeed(List<ProductListing> listings, String header) { 49 System.out.println("=".repeat(66)); 50 System.out.println(" " + header + " (" + listings.size() + " listings)"); 51 System.out.println("=".repeat(66)); 52 listings.forEach(l -> System.out.println(" " + l)); 53 System.out.println("=".repeat(66)); 54 } 55 56 public static void main(String[] args) { 57 58 List<ProductListing> feed = new ArrayList<>(); 59 feed.add(new ProductListing(" cotton kurti set ", "Fashion", 899.00)); 60 feed.add(new ProductListing("BRASS DIYA", "HomeDecor", 79.00)); 61 feed.add(new ProductListing("organic face serum", "Beauty", 1_199.00)); 62 feed.add(new ProductListing("phone case", "Electronics", 0.50)); // suspicious price 63 feed.add(new ProductListing("stainless steel tiffin","Kitchen", 349.00)); 64 feed.add(new ProductListing("branded handbag", "Fashion", 750_000.00)); // suspicious price 65 feed.add(new ProductListing("yoga mat anti slip", "Sports", 599.00)); 66 67 printFeed(feed, "ORIGINAL FEED"); 68 69 forwardNormalise(feed); 70 System.out.println(); 71 printFeed(feed, "AFTER FORWARD NORMALISATION"); 72 73 backwardInsertPlaceholders(feed); 74 System.out.println(); 75 printFeed(feed, "AFTER BACKWARD PLACEHOLDER INSERTION"); 76 77 long flaggedCount = feed.stream().filter(ProductListing::hasIssue).count(); 78 System.out.printf("%n Flagged listings: %d | Placeholders inserted: %d%n", 79 flaggedCount, flaggedCount); 80 } 81}
Output:
==================================================================
  ORIGINAL FEED (7 listings)
==================================================================
    cotton kurti set    Fashion       Rs.   899.00
  BRASS DIYA            HomeDecor     Rs.    79.00
  organic face serum    Beauty        Rs.  1199.00
  phone case            Electronics   Rs.     0.50
  stainless steel tiffin Kitchen      Rs.   349.00
  branded handbag       Fashion       Rs. 750000.00
  yoga mat anti slip    Sports        Rs.   599.00
==================================================================

==================================================================
  AFTER FORWARD NORMALISATION (7 listings)
==================================================================
  Cotton kurti set      Fashion       Rs.   899.00
  Brass diya            HomeDecor     Rs.    79.00
  Organic face serum    Beauty        Rs.  1199.00
  Phone case            Electronics   Rs.     0.50  [FLAG]
  Stainless steel tiffin Kitchen      Rs.   349.00
  Branded handbag       Fashion       Rs. 750000.00  [FLAG]
  Yoga mat anti slip    Sports        Rs.   599.00
==================================================================

==================================================================
  AFTER BACKWARD PLACEHOLDER INSERTION (9 listings)
==================================================================
  Cotton kurti set      Fashion       Rs.   899.00
  Brass diya            HomeDecor     Rs.    79.00
  Organic face serum    Beauty        Rs.  1199.00
  REVIEW REQUIRED       Electronics   Rs.     0.00
  Phone case            Electronics   Rs.     0.50  [FLAG]
  Stainless steel tiffin Kitchen      Rs.   349.00
  REVIEW REQUIRED       Fashion       Rs.     0.00
  Branded handbag       Fashion       Rs. 750000.00  [FLAG]
  Yoga mat anti slip    Sports        Rs.   599.00
==================================================================

  Flagged listings: 2 | Placeholders inserted: 2

Performance Considerations

OperationArrayList ListIteratorLinkedList ListIterator
next()O(1) — array indexO(1) — node.next
previous()O(1) — array indexO(1) — node.prev
set(element)O(1) — array writeO(1) — node field write
add(element)O(n) — shifts rightO(1) — pointer update
remove()O(n) — shifts leftO(1) — pointer update
listIterator(index)O(n) for LinkedList — must traverse to indexO(n) — traverses from closer end

Key insight for LinkedList: add() and remove() at the current position are O(1) for LinkedList's ListIterator because it holds a direct Node reference — no traversal needed for the modification. For ArrayList, the same operations are O(n) because elements must shift. When a traversal pattern involves many mid-list insertions or removals, LinkedList + ListIterator is genuinely faster than ArrayList + ListIterator.

Memory: A ListIterator object is heavier than a plain Iterator by a few fields (the extra add flag tracking) but still lightweight — a few dozen bytes regardless of the list size.

Best Practices

Use listIterator(fromIndex) when only part of the list needs processing. Creating a ListIterator at position 0 and calling next() to skip to a known start position is O(n) for LinkedList. Calling list.listIterator(fromIndex) directly is O(1) — it constructs the cursor at the right position immediately for ArrayList and O(n/2) at worst for LinkedList. Skipping elements manually wastes time and makes intent less clear.

Never call set() immediately after add() or remove(). Both operations reset lastRet to -1, which makes the "last returned element" undefined. A subsequent set() call throws IllegalStateException. If you need to add an element and then update it, add it with the desired final value in the first place.

Prefer ListIterator over manual index loops for bidirectional scans on large LinkedLists. A for loop with linkedList.get(i) on a LinkedList is O(n) per access — the full loop is O(n²). ListIterator on LinkedList is O(n) total for a full traversal because it holds a direct Node reference and never re-traverses. This is one of the few cases where LinkedList genuinely outperforms ArrayList.

Keep the traversal direction consistent within a single pass. Mixing next() and previous() calls unpredictably in the same traversal produces confusing cursor behaviour and hard-to-debug off-by-one errors. If a single pass needs both directions, finish the full forward sweep before switching to backward, or use two separate ListIterator instances with defined starting positions.

Common Mistakes

Mistake 1 — Calling set() After add() or remove()

1List<String> list = new ArrayList<>(List.of("A", "B", "C")); 2ListIterator<String> it = list.listIterator(); 3 4it.next(); // returns "A" 5it.remove(); // removes "A" — lastRet is reset to -1 6 7// WRONG — set() requires lastRet >= 0 (a prior next() or previous() since last add/remove) 8it.set("REPLACEMENT"); // throws IllegalStateException 9 10// CORRECT — call next() or previous() before set() 11it.next(); // returns "B" — lastRet is now 0 (after "A" was removed) 12it.set("B-UPDATED"); // safe — replaces "B" with "B-UPDATED" 13System.out.println(list); // [B-UPDATED, C]

Mistake 2 — Expecting next() to Return an Element Added with add()

1List<String> items = new ArrayList<>(List.of("X", "Y", "Z")); 2ListIterator<String> it = items.listIterator(); 3 4it.next(); // returns "X", cursor → 1 5it.add("INSERTED"); // inserts at position 1, cursor → 2 6 7// WRONG assumption: next() will return "INSERTED" 8// The cursor was advanced past the inserted element 9String next = it.next(); // returns "Y" — INSERTED was skipped by design 10System.out.println("next after add: " + next); // Y 11 12// To retrieve the inserted element, call previous() after add() 13// add() → previous() returns the inserted element
Output:
next after add: Y

Mistake 3 — Using ListIterator on a Set or Queue

1Set<String> names = new HashSet<>(Set.of("Alice", "Bob", "Charlie")); 2 3// WRONG — Set does not have a listIterator() method 4// This does not compile: 5// ListIterator<String> it = names.listIterator(); // compile error 6 7// CORRECT — use iterator() for Set, not listIterator() 8Iterator<String> it = names.iterator(); 9// OR use for-each: 10for (String name : names) { 11 System.out.println(name); 12}

Mistake 4 — Structural Modification Outside the ListIterator

1List<String> products = new ArrayList<>(List.of("Laptop", "Mouse", "Keyboard")); 2ListIterator<String> it = products.listIterator(); 3 4it.next(); // "Laptop" 5 6// WRONG — modifying the list directly outside the iterator 7products.add("Monitor"); // increments modCount, but iterator's expectedModCount unchanged 8 9it.next(); // throws ConcurrentModificationException 10 11// CORRECT — use it.add() to insert during traversal 12ListIterator<String> safe = products.listIterator(); 13safe.next(); 14safe.add("Monitor"); // iterator manages the modCount sync internally

Interview Questions

Q1. What is the difference between Iterator and ListIterator in Java?

Iterator is the base traversal interface — forward-only, available on any Collection, three methods: hasNext(), next(), remove(). ListIterator extends Iterator and is specific to List — it adds backward traversal with hasPrevious() and previous(), cursor position awareness with nextIndex() and previousIndex(), in-place replacement with set(element), and mid-traversal insertion with add(element). ListIterator can also be started at any position via list.listIterator(index). It is available only on ArrayList, LinkedList, and Vector.

Q2. What does set() do in ListIterator and when does it throw?

set(element) replaces the element most recently returned by next() or previous(). The cursor does not move — only the element at lastRet is replaced. It throws IllegalStateException in two cases: if neither next() nor previous() has been called since the iterator was created, or if remove() or add() was called after the last next()/previous() (both reset lastRet to -1). set() is not a structural change — it does not increment modCount — so it does not invalidate the iterator.

Q3. What does add() do to the cursor position in ListIterator?

add(element) inserts the new element immediately before the element that would have been returned by next(). The cursor advances past the inserted element, so a subsequent next() call returns the element that was originally at the cursor position — not the inserted element. previousIndex() after add() returns the index of the inserted element, so calling previous() immediately after add() returns the inserted element. add() also resets lastRet to -1, preventing set() or remove() from following it.

Q4. Why is ListIterator only available on List and not Set or Queue?

ListIterator requires the collection to have a stable, integer-based positional model — nextIndex() and previousIndex() must return meaningful values, and listIterator(index) must construct the cursor at a specific position. Set has no defined ordering for most implementations and no index-based access. Queue has a defined order but no get(index) semantic. Only List guarantees a stable, sequential, zero-based index for all positions, which makes the positional cursor model of ListIterator coherent.

Q5. When would you use ListIterator over a standard index loop on ArrayList?

Use ListIterator over an index loop on ArrayList when: (1) bidirectional traversal is needed without reversing the list, (2) in-place replacement with set() is cleaner than list.set(i, value) with a manually tracked index, (3) mid-traversal insertion with add() is needed and you want the iterator to handle index bookkeeping, or (4) the code operates on List<E> generically without knowing whether it is ArrayList or LinkedList. For pure ArrayList with simple forward access, an index loop is equally efficient and sometimes more readable.

Q6. How does ListIterator handle ConcurrentModificationException compared to Iterator?

Exactly the same mechanism. ListIterator inherits the modCount/expectedModCount fail-fast behaviour from the underlying collection's iterator implementation. Structural changes outside the ListIterator — calling list.add() or list.remove() directly — increment modCount without updating the iterator's expectedModCount, causing ConcurrentModificationException on the next next() or previous() call. The ListIterator's own add() and remove() methods update expectedModCount after the structural change, so they are safe.

FAQs

Can you use ListIterator to reverse a List in Java?

Not directly — ListIterator does not reverse a list, it traverses backward. To reverse, use Collections.reverse(list) for in-place reversal, or iterate backward with list.listIterator(list.size()) and collect into a new list. ListIterator's backward traversal is useful when you need to process elements in reverse without changing the original order.

What happens if you call previous() at the start of the list?

previous() throws NoSuchElementException when the cursor is at position 0 — hasPrevious() returns false at that point. The pattern is the same as next() at the end: always guard previous() with a hasPrevious() check in a while loop.

Does ListIterator work with immutable lists like List.of()?

List.of() returns an immutable list. Calling listIterator() on it creates a ListIteratorhasNext(), next(), hasPrevious(), previous(), nextIndex(), and previousIndex() all work. However, add(), remove(), and set() throw UnsupportedOperationException because the list does not support modification. Read-only traversal in both directions works fine.

Is there a ListIterator equivalent for Map?

No. Map does not support ListIterator. The map equivalent for controlled bidirectional traversal is NavigableMapTreeMap's headMap(), tailMap(), descendingMap(), and descendingKeySet() provide range views and reverse-order iteration. For entry-level iteration, map.entrySet().iterator() is the standard approach.

Can set() and add() be called in the same iteration step?

add() followed by set() throws IllegalStateException because add() resets lastRet to -1. set() followed by add() is valid — set() does not reset lastRet. If you need both for the same logical position, call set() first, then add(). The inserted element will appear after the replaced element in forward order.

What is the time complexity of creating a ListIterator at a specific index?

For ArrayList, list.listIterator(index) is O(1) — it simply sets the cursor integer to index. For LinkedList, it is O(min(index, size - index)) — the implementation traverses from the closer end (head or tail) to reach the starting position. For a 1,000-element LinkedList, starting at index 500 requires 500 node traversals.

Summary

ListIterator<E> is the full-featured traversal interface for Java List implementations. It extends Iterator with six additional methods: backward traversal (hasPrevious(), previous()), cursor position reporting (nextIndex(), previousIndex()), in-place replacement (set(element)), and mid-traversal insertion (add(element)).

The cursor sits between elements, not on them. next() returns the element at the cursor and advances forward; previous() steps back and returns the element it passes. set() replaces the last-returned element without moving the cursor. add() inserts before the current forward position and advances the cursor past the inserted element — so the inserted element is never returned again by the same next() call.

For interviews: know the full method list and what distinguishes ListIterator from Iterator, explain when set() throws IllegalStateException, describe what add() does to the cursor position, and clarify why ListIterator is List-only. These questions appear in technical interviews both as standalone questions on Java collections and as part of data-structure traversal problems.

What to Read Next