The Iterator Design Pattern
The Iterator Design Pattern is designed to give access to the elements of an aggregate object sequentially without exposing their underlying representation.
The Iterator Design Pattern is also known as Cursor.
What problems does it solve?
You can travers/ access elements of aggregate objects (like custom collections) sequentially without exposing their internal structure. The actual traversing algorithm will be implemented in the aggregate objects.
Glossary:
- Aggregate - aggregate object or collection, which elements we want to iterate (list or some other “bag” with elements)
- Concrete Aggregate - specific aggregate object
- Iterator - the generic interface that allows us to iterate over the elements of the aggregate
- Concrete Iterator - specific iterator for the specific aggregate object
Pros:
- Hides the implementation of traversing
- Decouples collection classes and algorithms
- More flexible than internal iterators (i.e. you can implement an iterator that traverses elements backward or based on another specification or to traverse 2 collections at the same time)
Cons:
- The iterator may not be aware if elements change during the iteration
- It can be an overkill if you can actually use the internal iterators instead of creating you own external iterator
How to recognize it?
When you call a behavioral method and it sequentially returns instances of elements (from a different type) from some aggregate structure (list/ container).
Examples from Java API
All implementations of java.util.Iterator (thus among others also java.util.Scanner!).
All implementations of java.util.Enumeration
Scenarios
- When you need to traverse some custom collection of elements sequentially, based on specific business rule
- When you need multiple iterators that know how to travers elements of a custom collection
- When you need more complex way to traverse elements, than the provided inner iterators
Example 1
Let’s say we have a collection of books. Each book has a title and genre. We may want to iterate our collection by some keyword in the title or by genre. This can be done by implementing two different iterators. Let’s do it.
1). Create the Book class (this is the type of the elements of the aggregate type) and the Book Genre enum
@ToString
public class Book {
@Getter
private String title;
@Getter
private BookGenre genre;
public Book(String title, BookGenre genre) {
this.title = title;
this.genre = genre;
}
}
public enum BookGenre {
MYSTERY,
FANTASY,
POETRY
}
2). We need a generic iterator that will iterate through the collection elements
public interface BookIterator {
boolean hasNext();
Book next();
}
3). We need an interface for the book collection (out aggregate object)
public interface BookCollection {
void add(Book book);
void remove(Book book);
void clear();
BookIterator keywordIterator(String keyword);
BookIterator genreIterator(BookGenre genre);
}
4). We need an implementation of the collection. This is the place, where we are going to implement the both iterators. We will place them as private inner classes, in order not to expose the traversal.
public class BookCollectionImpl implements BookCollection {
private List<Book> elements = new ArrayList<>();
@Override
public void add(Book book) {
this.elements.add(book);
}
@Override
public void remove(Book book) {
this.elements.remove(book);
}
@Override
public void clear() {
this.elements.clear();
}
@Override
public BookIterator keywordIterator(String keyword) {
return new BookIteratorByKeyword(new ArrayList<>(this.elements), keyword);
}
@Override
public BookIterator genreIterator(BookGenre genre) {
return new BookIteratorByGenre(new ArrayList<>(this.elements), genre);
}
private class BookIteratorByKeyword implements BookIterator {
private List<Book> elements;
private String keyword;
private int position = 0;
public BookIteratorByKeyword(List<Book> elements, String keyword) {
this.elements = elements;
this.keyword = keyword.toLowerCase();
}
@Override
public boolean hasNext() {
while (this.position < this.elements.size()) {
Book b = this.elements.get(this.position);
if (b.getTitle().toLowerCase().contains(this.keyword)) {
return true;
}
this.position++;
}
return false;
}
@Override
public Book next() {
Book book = null;
if (this.position >= 0 && this.position < this.elements.size()) {
book = this.elements.get(this.position);
this.position++;
}
return book;
}
}
private class BookIteratorByGenre implements BookIterator {
private List<Book> elements;
private BookGenre genre;
private int position = 0;
public BookIteratorByGenre(List<Book> elements, BookGenre genre) {
this.elements = elements;
this.genre = genre;
}
@Override
public boolean hasNext() {
while (this.position < this.elements.size()) {
Book b = this.elements.get(this.position);
if (this.genre.equals(b.getGenre())) {
return true;
}
this.position++;
}
return false;
}
@Override
public Book next() {
Book book = null;
if (this.position >= 0 && this.position < this.elements.size()) {
book = this.elements.get(this.position);
this.position++;
}
return book;
}
}
}
5). A Demo class
public class _Main {
public static void main(String[] args) {
BookCollection collection = new BookCollectionImpl();
collection.add(new Book("The Ruin of Kings /A Chorus of Dragons, #1/ by Jenn Lyons", BookGenre.FANTASY));
collection.add(new Book("Black Leopard, Red Wolf (The Dark Star Trilogy)", BookGenre.FANTASY));
collection.add(new Book("The Bird King by G. Willow Wilson - Goodreads", BookGenre.FANTASY));
collection.add(new Book("The Strength In Our Scars, by Bianca Sparacino", BookGenre.POETRY));
collection.add(new Book("End of Watch /Bill Hodges Trilogy, #3/ by Stephen King", BookGenre.MYSTERY));
collection.add(new Book("The Bird King by G. Willow Wilson - Goodreads", BookGenre.MYSTERY));
System.out.println("========== Keyword Iterator - \"King\" =========");
BookIterator keywordIt = collection.keywordIterator("King");
traverseCollection(keywordIt);
System.out.println("========== Genre Iterator - \"Mystery\" =========");
BookIterator mysteryIt = collection.genreIterator(BookGenre.MYSTERY);
traverseCollection(mysteryIt);
System.out.println("========== Genre Iterator - \"Fantasy\" =========");
BookIterator fantasyIt = collection.genreIterator(BookGenre.FANTASY);
traverseCollection(fantasyIt);
}
private static void traverseCollection(BookIterator iterator) {
Book book;
while (iterator.hasNext()) {
book = iterator.next();
if (book != null) {
System.out.println(book);
}
}
System.out.println();
}
}
Output:
========== Keyword Iterator - "King" =========
Book(title=The Ruin of Kings /A Chorus of Dragons, #1/ by Jenn Lyons, genre=FANTASY)
Book(title=The Bird King by G. Willow Wilson - Goodreads, genre=FANTASY)
Book(title=End of Watch /Bill Hodges Trilogy, #3/ by Stephen King, genre=MYSTERY)
Book(title=The Bird King by G. Willow Wilson - Goodreads, genre=MYSTERY)
========== Genre Iterator - "Mystery" =========
Book(title=End of Watch /Bill Hodges Trilogy, #3/ by Stephen King, genre=MYSTERY)
Book(title=The Bird King by G. Willow Wilson - Goodreads, genre=MYSTERY)
========== Genre Iterator - "Fantasy" =========
Book(title=The Ruin of Kings /A Chorus of Dragons, #1/ by Jenn Lyons, genre=FANTASY)
Book(title=Black Leopard, Red Wolf (The Dark Star Trilogy), genre=FANTASY)
Book(title=The Bird King by G. Willow Wilson - Goodreads, genre=FANTASY)