Java ListIterator
Iterate Java lists in both directions and modify during iteration with the ListIterator interface.
ListIterator<E> extends Iterator<E> with everything a list can support that a generic iterable cannot: walking backwards, asking for the current index, and adding or replacing elements during iteration. It's available on every List<E> via list.listIterator() and list.listIterator(int startAt).
If you're iterating a Set or a Queue, this chapter doesn't apply — those collections have no positions. For List, ListIterator is the cursor that does everything the plain Iterator does, plus the four list-specific operations.
What ListIterator adds
public interface ListIterator<E> extends Iterator<E> {
// inherited:
boolean hasNext();
E next();
void remove();
// new:
boolean hasPrevious();
E previous();
int nextIndex();
int previousIndex();
void set(E e);
void add(E e);
}Three new capabilities:
- Bidirectional walk.
hasPrevious()/previous()move the cursor backwards.previous()throwsNoSuchElementExceptionpast the start. - Position reporting.
nextIndex()returns the indexnext()would return;previousIndex()returns the indexprevious()would return. They differ by 1. - In-place edits.
set(e)replaces the element most recently returned bynextorprevious.add(e)inserts a new element between the previous and next cursor positions.
The cursor model
The trick to understanding ListIterator is to picture the cursor as sitting between elements, not on them:
[ "a" "b" "c" ]
^ ^ ^ ^
0 1 2 3 <- nextIndex() valuesnext() returns the element to the right of the cursor and advances. previous() returns the element to the left and moves back. Just after next() returns "b":
[ "a" "b" "c" ]
^
previousIndex()=1, nextIndex()=2A subsequent set("B") replaces "b". A subsequent add("x") inserts "x" between "b" and "c". A subsequent remove() deletes "b". Only one of set, add, or remove can be called once per next/previous — calling two in a row, or calling any of them without an intervening next/previous, throws IllegalStateException.
Bidirectional traversal
List<String> letters = new ArrayList<>(List.of("a", "b", "c"));
ListIterator<String> it = letters.listIterator();
while (it.hasNext()) System.out.print(it.next() + " "); // a b c
while (it.hasPrevious()) System.out.print(it.previous() + " "); // c b aBoth loops use the same iterator. After the forward loop ends, the cursor is past "c"; the backward loop starts there and walks home. You can flip direction mid-walk too — call next() then previous() and you get the same element back both times, because the cursor moved past it and then back over it.
In-place editing during iteration
This is the headline reason to use ListIterator instead of a plain Iterator:
List<String> words = new ArrayList<>(List.of("alpha", "beta", "gamma"));
ListIterator<String> it = words.listIterator();
while (it.hasNext()) {
String w = it.next();
if (w.startsWith("a")) it.set(w.toUpperCase()); // replace in place
if (w.equals("beta")) it.add("BETA-extra"); // insert after beta
}
// words is now [ALPHA, beta, BETA-extra, gamma]set is the only safe way to replace an element during iteration. add is the only safe way to insert an element during iteration. Both update the iterator's internal expected-modification count, so neither triggers a ConcurrentModificationException.
add deserves a second look: it inserts at the cursor — between the most recent next result and the next next result. After the insertion, the cursor is past the new element, so the very next it.next() returns the original next element, not the one you just added. That's almost always the behaviour you want when you're "expanding" an element in place.
A common gotcha: previous() returns the same element you just returned with next()
ListIterator<String> it = letters.listIterator();
it.next(); // "a", cursor between a and b
it.previous(); // "a" again, cursor between (start) and aThis trips people up. The cursor's position changes after next, but previous walks back over the same element. If you want the element before the current one, you have to call previous twice — once to step back over what you just returned, once to actually read the previous.
Starting at a specific index
ListIterator<String> it = list.listIterator(3); // start with cursor before index 3The two-arg form positions the cursor before the given index. it.nextIndex() returns 3, it.previousIndex() returns 2, and the very first next() returns list.get(3). Useful when you've already located a starting point with indexOf or binarySearch and want to walk from there in either direction.
LinkedList vs ArrayList: same interface, different cost
Both expose ListIterator. The cost profile differs:
ArrayList—next/previousare O(1);add/removeduring iteration are O(n) because they shift the array tail. Thesetoperation stays O(1).LinkedList—next/previousare O(1) (the iterator caches the node);add/removevia the iterator are O(1) because no shifting happens. The same operations via index on aLinkedListare O(n) — index lookups walk the chain.
If you find yourself iterating a LinkedList and calling list.add(index, ...) inside the loop, you're walking the chain twice per insertion. Use the ListIterator and you're paying O(1) per operation, which is the whole reason LinkedList exists.
A worked example: bidirectional walk, in-place edits, index reporting, costed ops
The program below walks a list forwards and backwards on the same iterator, replaces and inserts elements in-place, reports indices as it goes, and times the difference between iterator-based and index-based modification on a LinkedList.
What to take from the run:
- The forward and backward walks happen on the same
ListIterator. Once the forward loop exhaustshasNext, the cursor is past the last element andhasPreviousbecomes true. setreplaced"alpha"with"ALPHA"andadd("BETA-extra")inserted a new element right after"beta"— and the iterator survived both modifications without aConcurrentModificationException.next()thenprevious()returned the same element. The cursor moved past it and then back over it; what looked like two reads of "different" elements is actually one element traversed twice.- On a
LinkedList, the iterator-based version of "drop every other element" was dramatically faster than the index-based version. Index lookup on a linked list is O(n); the iterator caches its node and the deletion is O(1).
What's next
Iterator and ListIterator handle the traversal side of working with collections. The other half of "doing things with elements" is ordering them: telling Java when one element is less than, equal to, or greater than another. That's what Comparable and Comparator cover — natural order baked into the type itself, and external orderings you supply per operation. They're the foundation everything else in this part of the book rests on, including the sort and search utilities.