Concepts / Generic Programming with Collection<T>

Generic Programming with Collection<T>

An iterator is an object that traverses a collection, and the same calling pattern works across different collection types.

  • Programming

Writing One Loop for Many Storage Forms

Suppose you want one method that prints every item in a collection, no matter how that collection is stored. This article starts with the code that solves that problem, then explains the mechanism behind it. Read the code below first. The variable coll is assumed to be a collection of Strings. Do not worry yet about why each line is needed. Notice only how the loop is structured: it asks the iterator whether anything is left, takes one item, and does something with it.

java

What do you think happens?

Once the loop has already printed the last item in the collection, what does iter.hasNext() return on the next check?

  • true

  • false

Reveal answer
Answer:

false

hasNext() reports whether items remain. After the last item has been returned by next(), no items remain, so hasNext() returns false and the while loop stops.

Why One Method Cannot Walk Every Structure

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 can take these and many other forms, a method written for one form cannot simply be reused for another. A loop written for array indices does not describe how to visit the nodes of a tree.

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. The difference between a linked list walk and a tree walk is hidden inside the iterator, and the loop you write never has to see it.

Tracing the Cursor Through a Collection

A helpful way to picture an iterator is as a cursor that sits between items. It starts before the first item. Each call to next() moves the cursor forward past one item and hands that item back to you. Because of this, next() both reads and advances. The interface offers no way to peek at an item without consuming the step. Every look at an item is also a step forward.

redindex 0, just returned bynext()cursorafter red, before index 1nullindex 1, not yet returnedblueindex 2, not yet returnednext() moves cursor forwardnext() returns this item
Where is the iterator's position after each call to next(), and why does calling remove() act on the element just returned rather than the next one?

The diagram shows the cursor after one call to next(). The item red has been returned, and the cursor now sits before null. The next call to next() will return null and advance past it. The remove() method, when available, deletes the item most recently returned by next(). That is why the cursor position matters: remove() always refers to the item just behind the cursor, not the item ahead of it.

The Three Iterator Methods

Definition

hasNext() reports whether items remain in the traversal. It returns true while there is at least one item the iterator has not yet returned, and false once every item has been returned.

Definition

next() returns the next item in the traversal and advances the iterator past it. Calling next() when no items remain is an error, which is why the loop checks hasNext() first.

Definition

remove() deletes the item most recently returned by next() from the collection. Whether this is allowed depends on the collection supporting removal through its 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, and each one plays a different role in the loop. The first two form the traversal itself, and the third changes the collection during that traversal.

The Order of Calls in the Loop

The diagram below shows the order of calls on each pass through the loop. Read it from top to bottom. The collection is asked for an iterator exactly once, before the loop begins. After that, the loop alternates between asking whether items remain and asking for the next one. The removal step is optional, so it appears only on passes where you choose to delete an item.

hasNext()items remain?next()returns item and advancesremove()optional, deletes lastreturned itemLoop endshasNext() returned falsetrueno removal this passif removingnext passfalse
In what order do hasNext(), next(), and remove() run on each pass, and what happens when hasNext() returns false?

Two details are easy to miss in the diagram. First, the hasNext() and next() pair repeats once for every item, which is why the loop runs as many times as there are items. Second, the loop never touches the collection's internal storage. Everything it needs passes through the iterator. That is the reason the same loop shape works for arrays, linked lists, and trees alike.

Removing Items While Walking

The same general form can be used for other kinds of processing. Suppose the goal is to remove every null value from a collection of Color objects. The removal is performed through the iterator, using iter.remove(), rather than by changing the collection directly during the walk. The source notes that this works as long as the collection supports removal. The following version is an original illustration of that pattern using the same loop shape.

java
Notes on the code
  • hasNext() decides whether to keep walking
  • next() returns the item and moves the cursor past it
  • remove() deletes the item that next() just returned, so it must come after next()
Worked example

Removing nulls from a four-item collection

The collection holds, in order, red, null, blue, and null. Trace the loop above and state what the collection holds when the loop ends.

  1. 1
    Start

    The cursor sits before red. hasNext() returns true because four items remain.

  2. 2
    First pass

    next() returns red and advances. c is not null, so remove() is not called. The collection still holds red, null, blue, null.

  3. 3
    Second pass

    hasNext() returns true. next() returns null and advances past it. c is null, so remove() deletes the item just returned, which is the first null.

  4. 4
    Third pass

    hasNext() returns true because blue and the second null remain. next() returns blue. c is not null, so nothing is removed.

  5. 5
    Fourth pass

    hasNext() returns true. next() returns the second null. c is null, so remove() deletes it.

  6. 6
    Final check

    hasNext() returns false because no items remain, so the while loop stops.

Result

The collection holds red and blue, in that order. Every null was removed, and the loop never called next() without first checking hasNext().

Mistakes Beginners Make

  • Common mistake

    Calling next() without checking hasNext() first

    What it looks like
    Iterator<String> iter = coll.iterator();
    String first = iter.next();
    String second = iter.next();  // may fail if only one item exists
    Why it fails

    next() assumes an item remains. When none remain, the traversal throws NoSuchElementException.

    Do this instead

    Put every next() call inside a loop guarded by while (iter.hasNext()).

  • Common mistake

    Expecting next() to peek at an item without moving forward

    What it looks like
    String item = iter.next();  // the cursor has already moved past this item
    Why it fails

    The interface has no peek operation. Every call to next() consumes a step.

    Do this instead

    Store the returned value in a variable if you need it again, and do not expect a second call to return it.

  • Common mistake

    Calling remove() before any item has been returned

    Why it fails

    remove() deletes the item most recently returned by next(), so it has nothing to act on until next() has run at least once.

    Do this instead

    Call remove() only after a next() call in the same pass, as in the Color example.

  • Common mistake

    Assuming every collection supports iter.remove()

    Why it fails

    Removal depends on the collection supporting it, and an unsupported removal may throw UnsupportedOperationException.

    Do this instead

    Check the collection's documentation, or handle the exception, before depending on removal.

Practice: Trace a Removal

MEDIUM

A collection holds, in order, blue, null, null, green. Using the removal loop from this article, list the value returned by each call to next(), the item removed on each pass where removal happens, and the final contents of the collection.

Hints
  • Write one line per pass, starting from the cursor before blue.
  • Remember that remove() deletes the item returned by the most recent next() call, not the item ahead of the cursor.
  • The loop ends only when hasNext() returns false.

Key Takeaways

  • An iterator is an object that traverses a collection, and each collection type supplies its own iterator internally, so the same calling pattern works across collection types.
  • hasNext() reports whether items remain, next() returns the next item and advances the iterator, and remove() deletes the item most recently returned by next().
  • Checking hasNext() before every call to next() keeps the traversal from throwing NoSuchElementException.
  • Removal through an iterator depends on the collection supporting it, and an unsupported removal may throw UnsupportedOperationException.