Concepts / Iterator

Iterator

A ListIterator moves in both directions and can add, set, and remove items, and it uses the same cursor model for all of them.

  • Programming

Why One Loop Shape Can Serve Many Collections

Suppose you want a single method that prints every item in a collection, no matter how that collection is stored. The difficulty is that each familiar storage form has its own way of being walked. An array is walked with a for loop over its indices. A linked list is walked with a while loop that moves a pointer from node to node. A binary tree is walked with a recursive subroutine that performs an inorder traversal. Because collections take these and many other forms, a method written for one form cannot simply be reused for another.

The solution this course uses is the iterator. An iterator is an object whose job is to traverse a collection. Each collection type supplies its own iterator internally, so the details differ from one type to another. What stays constant is how you call the iterator. Once you learn that calling pattern, the same loop shape works for any collection that provides an iterator. This article starts with the calling pattern, then explains the cursor model behind the more powerful ListIterator used for lists.

The Calling Pattern Behind Every Iterator

The Iterator interface is parameterized, written Iterator<T>, where T is the type of the items. If a collection implements Collection<T> for some type T, then calling coll.iterator() returns an Iterator<T> with that same T. The interface defines exactly three methods that matter for a basic traversal, and each plays a different role in the loop. The method hasNext() reports whether any items remain. The method next() returns the next item and advances the iterator. The method remove() deletes the item most recently returned by next().

java

Walking the Loop Step by Step

Read the loop from top to bottom as a conversation between the loop, the iterator, and the collection. The collection is asked for an iterator exactly once. After that, the loop alternates between asking whether items remain and asking for the next one. The loop never touches the collection's internal storage. Everything it needs passes through the iterator, which is why the same loop shape works for arrays, linked lists, and trees alike.

Worked example

Printing a collection with the generic loop

A collection coll holds the Strings apple, mango, and plum in that order. Trace what the while loop prints.

  1. 1
    First check

    hasNext() returns true because items remain. The iterator sits before apple.

  2. 2
    First pass

    next() returns apple and moves past it. The loop prints apple.

  3. 3
    Second pass

    hasNext() is true. next() returns mango, and the loop prints mango.

  4. 4
    Third pass

    next() returns plum, and the loop prints plum.

  5. 5
    Final check

    hasNext() returns false because nothing is left, so the loop ends without calling next() again.

Result

The loop prints apple, mango, and plum, one per line, in list order.

Where the ListIterator Cursor Sits

Lists keep their elements in a definite order, and many tasks need to move both forward and backward, or to insert and replace items at a chosen spot. For that reason Java provides a richer kind of iterator for lists. The method list.listIterator() returns a ListIterator<T>, an interface that extends Iterator<T>. Everything an ordinary iterator can do, a ListIterator can also do, and it adds more.

The most useful mental model is to stop thinking of an iterator as sitting on an item. Instead, think of it as pointing to a position. That position is either at the very beginning of the list, at the very end, or in the gap between two neighboring elements. Items are never under the iterator in this model; the iterator sits in the spaces between them. A list with n items always has n plus 1 possible cursor positions. For the three fruits apple, mango, and plum, there are four positions: before apple, between apple and mango, between mango and plum, and after plum. Keeping this count in mind makes insertion behavior predictable.

Gap 0before appleappleitem 0Gap 1between apple and mangomangoitem 1Gap 2between mango and plumplumitem 2Gap 3after plum
Where does the cursor sit among apple, mango, and plum, and how many gap positions exist?

The cursor structure is practical because of how LinkedList<T> stores its items. Its nodes are doubly linked: each node holds a pointer to the next node and a pointer to the previous node. Because of those two pointers, the iterator can step forward and backward through the nodes without restarting from the front. LinkedList also keeps a tail pointer that marks the last node, which speeds up addLast() and getLast(), but that tail pointer is not needed for iterator movement itself.

How next, previous, add, set, and remove Act

Every method acts relative to the cursor's current gap. The two movement methods cross one item and hand that item back to you. The method next() moves one space to the right and returns the item it just passed. The method previous() moves one space to the left and also returns the item it passed. Both return values are what you can inspect while traversing.

The editing methods are where beginners most often get confused, because remove() and set() act on the item the cursor passed most recently, not on the gap itself. The method remove() deletes that most recently passed item, whether it was passed by next() or previous(). The method set(obj) replaces that same item instead of deleting it. The method add(obj) behaves differently: it inserts obj at the cursor's current gap, either between two existing items or at the front or end of the list. Remove and set need a recently passed item to act on, while add needs only the gap. That is why the sorted-insertion technique uses add() after the cursor has been positioned, and does not need next() immediately before it.

MethodWhat it acts onEffect on the listCursor after the call
next()The gap, crossing one item to the rightNo change to the itemsMoves one gap to the right, after the item it returned
previous()The gap, crossing one item to the leftNo change to the itemsMoves one gap to the left, before the item it returned
remove()The item most recently passedThat item is deletedStays in the gap where the deleted item was
set(obj)The item most recently passedThat item is replaced by objUnchanged, still beside the replaced item
add(obj)The current gapobj is inserted into that gapMoves past the new item, so the next next() returns the item after it

Effects of the ListIterator methods relative to the cursor

Tracing the Sorted-Insertion Routine

The sorted-insertion technique keeps a list in ascending order. The idea is to start the iterator at the front and walk forward past every item that is smaller than the new string. When the iterator reaches the first item that should come after the new string, it has found the right gap, and add() places the new string there. The only step that needs care is the comparison. The rule is simple: if newItem.compareTo(item) gives zero or a negative number, newItem belongs before item.

java

Follow the two directions the routine allows. If the new string is bigger than the current item, the loop keeps advancing. If the new string is less than or equal to the current item, the routine steps the iterator back one space with previous(), so the cursor sits in the gap just before that item, and then leaves the loop. The other exit happens when hasNext() is false, which means every item was smaller. The iterator is then already at the end, so add() appends the new string.

Worked example

Inserting dog into a sorted list with an existing dog

The sorted list holds cat, dog, fox. Insert a new dog using the sorted-insertion routine and predict where it lands.

  1. 1
    Pass 1

    next() returns cat and moves the cursor past it. dog.compareTo(cat) is positive, so the loop continues.

  2. 2
    Pass 2

    next() returns the existing dog and moves past it. dog.compareTo(dog) is zero. The rule treats zero as comes before, so the routine proceeds to step back.

  3. 3
    Step back

    previous() returns the existing dog and moves the cursor to the gap between cat and the existing dog.

  4. 4
    Insert

    The break leaves the loop, and add(dog) places the new dog in that gap, in front of the existing dog.

Result

The list becomes cat, dog, dog, fox. The new duplicate sits before the equal item, not after it.

What do you think happens?

In the dog example, if the comparison had used < 0 instead of <= 0, where would the new dog end up?

  • In front of the existing dog

  • After the existing dog, between dog and fox

  • At the end of the list

Reveal answer
Answer:

After the existing dog, between dog and fox

With a strict less-than test, compareTo returning zero would not trigger the step back. The loop would pass the existing dog, and the new dog would be inserted after it, giving cat, dog, dog, fox in the same order, but the placement of the inserted item relative to equal items would differ.

Startcursor before catnext() gives catdog is bigger, continuenext() gives dogdog compareTo dog is zeroprevious() gives dogcursor between cat and dogadd(dog)new dog placed herecat, dog, dog, foxfinal listhasNext truehasNext truezero means beforebreak
As each existing item is compared with the new dog, where does the cursor move next, and at which step is the new dog inserted?

Where Sorted Insertion Goes Wrong

The routine is short, so each mistake usually shows up as a subtle wrong position rather than a crash. The mistakes below are the ones most likely to produce an insertion that is almost right but out of order.

  • Common mistake

    Leaving out the previous() call before add()

    What it looks like

    The sorted list is cat, dog, fox and the new item is dog. The loop stops at the existing dog without stepping back, then add() places the new dog after the existing dog.

    Why it fails

    After next() returns an item, the cursor sits after that item. add() then inserts on the far side of it, so the new item lands one position too late.

    Do this instead

    When the comparison shows the insertion point, call previous() once so the cursor sits in the gap before the item, then call add().

  • Common mistake

    Using a comparison that treats equal items as come after

    What it looks like

    The test is written as newItem.compareTo(item) < 0. With the new dog, the existing dog gives zero, so the loop passes it.

    Why it fails

    The rule in this technique treats zero as comes before. A strict test changes where equal items are placed, so duplicates end up in a different order than intended.

    Do this instead

    Test for zero or a negative result with compareTo(item) <= 0, so a new duplicate lands before equal items.

  • Common mistake

    Calling previous() when the comparison has not yet shown the insertion point

    What it looks like

    previous() is called on every pass, even when the new item is bigger than the current item.

    Why it fails

    This moves the cursor backward each time, so the iterator keeps retracing items it has already passed and the insertion point is never reached in order.

    Do this instead

    Call previous() only inside the branch where newItem.compareTo(item) is zero or negative, then leave the loop.

  • Common mistake

    Expecting add() to use the last item returned by next()

    What it looks like

    next() returns owl and the programmer assumes add(cow) will go in front of owl.

    Why it fails

    add() acts on the current gap, not on the most recently returned item. After next(), the gap sits after owl, so the item goes after it.

    Do this instead

    Use previous() to move the cursor back into the gap before the item, then add().

Removing Items While Walking

The same general loop can also remove items. Doing the removal through the iterator, with iter.remove(), keeps the traversal consistent. Changing the collection directly during the walk bypasses the iterator's bookkeeping about its position. The example below removes every null value from a collection of Color objects.

Code walkthrough
Iterator<Color> iter = coll.iterator();
while (iter.hasNext()) {
    Color c = iter.next();
    if (c == null) {
        iter.remove();
    }
}
  1. 1
    The iterator is obtained once from the collection.
  2. 2
    hasNext() decides whether to take another pass.
  3. 3
    next() returns the item for this pass.
  4. 4
    If the item is null, remove() deletes that exact item through the iterator.
  5. 5
    The loop then tests hasNext() again and continues with the remaining items.
  • hasNext() guards each call to next(), so the loop never reads past the end
  • next() returns the current item and advances, so the removal acts on it
  • iter.remove() deletes the item that next() just returned, and only when it is null
MEDIUM

Predict what happens if you write the same removal loop without iter.remove(), and instead call coll.remove(c) directly from inside the loop. Which mistake from the previous section does this resemble?

Hints
  • Think about what the iterator remembers about its position after you change the collection behind its back.
  • The safe pattern in this article keeps every removal inside the iterator.

Practice Tracing an Insertion

MEDIUM

The ascending list ant, bee, owl is traversed with the sorted-insertion routine to add cow. List every item the loop passes, state where previous() leaves the cursor, and give the final list.

Hints
  • cow is bigger than ant and bee, so those two items are passed by next().
  • At owl, cow.compareTo(owl) is negative, so the routine steps back with previous().

Key Takeaways

  • An iterator traverses a collection through one calling pattern, hasNext(), next(), and remove(), so the same loop shape works across different collection types.
  • A ListIterator's cursor sits in one of n+1 gaps in a list of n items. next() and previous() cross an item, remove() and set() act on the item just passed, and add() inserts into the current gap.
  • Sorted insertion walks forward past smaller items, steps back with previous() when compareTo shows the insertion point, and then calls add().
  • Removal should go through iter.remove() inside the loop, and only where the collection supports it.