When we write programs, we often create classes that contain collections of various types, such as arrays, lists, hashmaps, and so on. We often need to access those collections from other parts of our code and iterate over them, for example, to modify them, calculate the sum of their values or apply some other methods to them. Wouldn't it be great if we could have a universal interface to access elements of any collection contained in a Java class? Luckily, the Iterator design pattern provides such an interface, and we are going to investigate it in detail in this topic.
Simple problem
First of all, let us imagine a class that represents a range of dates.
import java.time.LocalDate;
class DateRange {
private final LocalDate from;
private final LocalDate to;
DateRange(String from, String to) {
this.from = LocalDate.parse(from);
this.to = LocalDate.parse(to);
}
}
Now let's think about how we can access that range, for example, to print it to the console. For instance, we could simply make an array containing all the dates within the range and expose it so that we could access it from other classes. So we just add the following method to DateRange:
import java.time.temporal.ChronoUnit;
import java.time.LocalDate;
class DateRange {
// fields, constructor
public LocalDate[] getRange() {
int length = (int) ChronoUnit.DAYS.between(from, to);
LocalDate[] range = new LocalDate[length];
LocalDate start = from;
for (int i = 0; i < length; i++) {
range[i] = start;
start = start.plusDays(1);
}
return range;
}
}
Once it's done, let's create a special class that will be responsible for printing the range of dates exposed as an array:
class DatePrinter {
public static void print(LocalDate[] array) {
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
}
}
If we test this implementation, we'll see that it works as intended:
DateRange range = new DateRange("2022-01-01", "2022-01-05");
DatePrinter.print(range.getRange());
Here is what it will print:
2022-01-01
2022-01-02
2022-01-03
2022-01-04
Looks fine and doesn't promise any trouble, right? But what if after a while we are not satisfied with the current implementation of DateRange and decide that exposing the range as List<LocalDate> is more convenient? If we change the implementation detail of the getRange method, it will break the client code: currently, it consumes an array but the range is going to be exposed as a List. In our example, we have only one method that consumes the range but imagine what will happen if we have multiple consumers in different parts of our program!
Do we have a better option? Fortunately, yes.
Clever design
We can use Iterator, a behavioral design pattern invented to overcome all the difficulties we discussed above. The idea of this pattern is very simple. We can define a universal interface called Iterator to describe the capabilities we need from concrete iterators. We can implement this interface in different classes where we can put all iteration-related logic. On top of it, we can add a new method to any classes whose data we want to iterate, and that method will have the Iterator return type but will return instances of concrete implementations of the Iterator interface.
You can see how all the components come together in this diagram:
Here, the Iterator is the iterator interface and the ConcreteIterator is a class representing its concrete implementation. The Aggregate is an interface that gives us a hint that any class implementing it can produce an iterator. The ConcreteAggregate is a class that implements the createIterator method of the Aggregate interface to return a certain ConcreteIterator. Finally, the Client is the code where the Aggregate and the Iterator are called.
Let's try to reproduce this diagram in practice.
Practical solution
Let's return to our DateRange class. First of all, we don't really need to create a collection of all dates in the range. Instead, we can keep just the start and the end values of the range and move all the logic related to iteration over the range to a separate class implementing the DateIterator interface.
interface DateIterator {
boolean hasNext();
LocalDate next();
}
Now, let's create a class implementing DateIterator and make it an inner class of DateRange. Also, we are going to add a new method to DataRange, which will return an instance of DateIterator:
class DateRange {
// fields, constructor, getters and setters
public DateIterator iterator() {
return new DateIteratorImpl();
}
private class DateIteratorImpl implements DateIterator {
private LocalDate current;
private DateIteratorImpl() {
current = from;
}
@Override
public boolean hasNext() {
return current.isBefore(to);
}
@Override
public LocalDate next() {
LocalDate prev = current;
current = current.plusDays(1);
return prev;
}
}
}
It doesn't mean that the whole iteration logic now resides in a dedicated class. This way we will respect the single responsibility principle, and any breaking changes in the implementation of the DateRange class will affect only the implementation of the associated iterator class but not the client code. In fact, users of the DateRange class won't be able to notice any changes until they peek into the source code.
We will have to modify the print method to accept DateIterator:
public static void print(DateIterator iterator) {
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
Note that we can have multiple implementations of DateIterator in the DateRange class. For example, we can have a specialized iterator that exposes only weekends and a specialized iterator that exposes only national holidays within the range. From the client's point of view, they will behave in the same way, and the complex logic of identifying specific days will be hidden inside these iterator implementations.
Iterator<E>, a generic interface in the java.util package, which is commonly used for working with collections. If you are going to implement the Iterator pattern, we recommend using the available Java interface because it will allow for easy integration with other Java libraries and frameworks.
Conclusion
In this topic, you learned how to implement and use one of the most popular design patterns in Java. Of course, if you have a simple collection without any complex iteration logic, you can easily go without the Iterator pattern. However, if you need to implement a complex iteration logic or a special data structure, you might want to use this pattern to make working with your code simple and predictable.
Don't be surprised if you find the Iterator pattern familiar — it is widely used, for instance, in the Collections framework, and it's likely you have already encountered it many times.