This lab was written by Prof. John Bowers.
Introduction
As you saw in CS 159, in addition to the the standard for
and while
looping structures, Java
supports the “for-each” loop structure for iterating over all elements in a collection. The following method, for example, prints out all integers passed into it through its list parameter:
public void printAll(ArrayList<Integer> list) {
for (int i : list) {
System.out.println(i);
}
}
This syntactic sugar is a lot more convenient than the typical for
loop:
public void printAll(ArrayList<Integer> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
All of the standard Java Collections Framework classes (ArrayList, LinkedList, HashMap, etc.) work with for-each loops. In this lab, you will learn how to make your own iterators and use them to loop over a custom collection.
What is an iterator?
Recall from CS 159 that the iterator design pattern abstracts the idea of “looping” over an aggregate object like an ArrayList or HashSet. The iterator pattern provides methods for retrieving the next item (next()
) as well as checking whether there is a next item (hasNext()
). A typical loop using an iterator looks like:
// Assuming we have a List<Integer> type object list:
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer nextInt = iterator.next();
// ...
}
The code above gets an Iterator
object from the list
using the .iterator()
method. It then uses the iterator’s hasNext()
and next()
methods to loop over all of the integers stored in list
.
The purpose of the iterator pattern is to hide the underlying implementation specific details about how to loop over a particular data structure and allow the programmer to think just about the high level loop structure–we don’t think “to iterate over this list, I need to use an integer index and add one to it each time.” No. Instead we think “to iterate over this list I’ll keep getting the next item until there aren’t any more.” This greatly simplifies the job of the programmer using our code.
Creating an iterator
In order to create an iterator in Java, you subclass java.util.Iterator
and implement the next()
and hasNext()
methods. Here is a simple example of a class that provides an iterator for looping over a fixed-sized array of message strings:
import java.util.Iterator;
import java.util.NoSuchElementException;
public class Messages {
String[] messages = new String[10];
public Messages() {
for (int i = 0; i < 10; i++) {
messages[i] = "Message #" + i;
}
}
private class MessageIterator implements Iterator<String> {
int index;
public MessageIterator() {
index = 0;
}
@Override
public boolean hasNext() {
return index < 10;
}
@Override
public String next() {
if (hasNext()) {
return messages[index++];
} else {
throw new NoSuchElementException();
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
public Iterator<String> iterator() {
return new MessageIterator();
}
public static void main(String[] args) {
Messages theMessages = new Messages();
Iterator<String> iterator = theMessages.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
Our iterator is implemented in the private inner class MessageIterator
. The MessageIterator
keeps track of what index
in the list it is currently on. A call to hasNext()
checks whether index
is valid (i.e. is less than 10). A call to next()
returns the value stored at messages[index]
, if index
is valid, or otherwise throws a NoSuchElementException
. The remove()
method is essentially optional (in Java 8, it actually is optional, but we are using Java 7 in this class), but we have to implement it in order to implement the Iterator
interface. This is done by having it throw the UnsupportedOperationException
, which keeps it from being used.
Notice that the only purpose of having the inner class MessageIterator
is to support the iterator()
method. We really don’t even need to give this class a name. We make the code a bit cleaner by defining the inner class as an anonymous inner class within the iterator()
method, like so:
import java.util.Iterator;
import java.util.NoSuchElementException;
public class Messages {
String[] messages = new String[10];
public Messages() {
for (int i = 0; i < 10; i++) {
messages[i] = "String #" + i;
}
}
public Iterator<String> iterator() {
return new Iterator<String>() {
int index = 0;
@Override
public boolean hasNext() {
return index < 10;
}
@Override
public String next() {
if (hasNext()) {
return messages[index++];
} else {
throw new NoSuchElementException();
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
public static void main(String[] args) {
Messages theMessages = new Messages();
Iterator<String> iterator = theMessages.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
Now, consider the loop structure in main
that loops over all of the messages using the iterator and prints them out. Our Messages
class is really an aggregate of ten messages. It would be convenient if we could use a for-each to loop over the messages:
Messages theMessages = new Messages();
for (String message : theMessages) {
System.out.println(message);
}
To support the for-each loop, we need to make our class implement the Iterable
interface, which provides an iterator()
method. The for-each loop uses the iterator()
method in order to build a normal while-loop structure as in our original code. Here is the listing of our code now modified to implement iterable()
and then use a for-each loop:
import java.util.Iterator;
import java.util.NoSuchElementException;
public class Messages implements Iterable<String> {
String[] messages = new String[10];
public Messages() {
for (int i = 0; i < 10; i++) {
messages[i] = "String #" + i;
}
}
@Override
public Iterator<String> iterator() {
return new Iterator<String>() {
int index = 0;
@Override
public boolean hasNext() {
return index < 10;
}
@Override
public String next() {
if (hasNext()) {
return messages[index++];
} else {
throw new NoSuchElementException();
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
public static void main(String[] args) {
Messages theMessages = new Messages();
for (String message : theMessages) {
System.out.println(message);
}
}
}
That’s it! Almost nothing changed since we had already named our iterator method iterator()
, we only had to add implements Iterable
to our class signature and add an @Override annotation to iterator()
to tell the compiler that it implements the iterator()
method from Iterable
.
Now it gets awkward
As we saw above, having a class implement Iterable
and return an anonymously defined Iterator
object is very straightforward. An Iterator
is an object that allows us to iterate over some aggregate (or collection). An Iterable
is an aggregate that provides an Iterator
of some sort. Easy, no?
Well, not quite. The problem comes when we want to support more than one type of iterator for the same aggregate type. Why is this a problem? Well, consider trying to add a second type of iterator to Messages
. One that only iterates over the last five messages. In order to use a for-each loop with Messages
we made it implement the Iterable
interface and provided an implementation of iterator()
. But that’s all we get. We can’t provide a second implementation of iterator()
in the same class that returns some other sort of iterator. Instead, and here’s where it gets awkward, we have to create a method which returns an Iterable
object wrapped around an Iterator
. Study the following example to see what this means:
import java.util.Iterator;
import java.util.NoSuchElementException;
public class Messages implements Iterable<String> {
String[] messages = new String[10];
public Messages() {
for (int i = 0; i < 10; i++) {
messages[i] = "String #" + i;
}
}
public Iterator<String> iterator() {
return new Iterator<String>() {
int index = 0;
@Override
public boolean hasNext() {
return index < 10;
}
@Override
public String next() {
if (hasNext()) {
return messages[index++];
} else {
throw new NoSuchElementException();
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
/**
* An Iterable object for constructing an iterator that loops over the
* last five messages.
*/
public class LastFiveIterable implements Iterable<String> {
@Override
public Iterator<String> iterator() {
return new LastFiveIterator();
}
}
/**
* An iterator for iterating over the last five messages.
*/
public class LastFiveIterator implements Iterator<String> {
int index;
public LastFiveIterator() {
index = 5;
}
@Override
public boolean hasNext() {
return index < 10;
}
@Override
public String next() {
if (hasNext()) {
return messages[index++];
} else {
throw new NoSuchElementException();
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
/**
* @return An iterable object for iterating over the last five messages.
*/
public Iterable<String> lastFive() {
return new LastFiveIterable();
}
public static void main(String[] args) {
Messages theMessages = new Messages();
System.out.println("Printing all the messages:");
for (String message : theMessages) {
System.out.println(message);
}
System.out.println("\nPrinting the last five messages: ");
for (String message : theMessages.lastFive()) {
System.out.println(message);
}
}
}
Notice the difference between iterator()
and lastFive()
. The iterator()
method simply returns an Iterator
object, whereas the lastFive()
method returns a LastFiveIterable
object that itself implements a second iterator()
method that returns a LastFiveIterator
object. If you need to support more than one iterator, this is (awkwardly), the Java way of doing it: all of your extra iterators need two components–an iterator type and an iterable type, which acts as a factory for iterators.