Learn Java

Java Iterator

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. To do this, we can the getRange()  to DateRange as shown below:

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;
    }
}

After the getRange() method is ready, 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 whether 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 seems unlikely to cause 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:

Iterator 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;
        }
    }
}

This doesn't mean that the whole iteration logic now resides in a dedicated class. Because we want to adhere to the single responsibility principle. Any major changes (capable of breaking the client code) 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 as well:

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.

In Java, we have 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.

Using iterators

The Iterator<T> is a universal mechanism for iterating over collections regardless of their structure. It takes elements in the order provided by the collection. In some sense, it is like a moveable "pointer" to an element of the collection.

The iterator allows you to remove elements from the underlying collection, but you cannot do it using a for-each loop.

Some methods of the Iterator<E> interface are:

  • boolean hasNext() — returns true if the iteration has more elements, and false otherwise;
  • E next() — returns the next element in the iteration;
  • void remove() — removes the last element returned by this iterator from the collection.

The for-each loop uses the first two methods under the hood.

It is also possible to directly access and use an iterator of a collection. The typical usage includes three steps:

  1. Check the collection has the next element;
  2. Obtain the next element;
  3. Process the obtained element.

An iterator for lists

There is a special iterator for lists called ListIterator which extends the common Iterator interface. It allows the programmer to traverse the list in either direction, modify the list during iteration, and obtain the current position in the list.

In addition to standard methods of the Iterator class, this iterator provides the following methods:

  • int nextIndex() — returns the index of the element that would be returned by invoking next;
  • boolean hasPrevious() — returns true if the list has more previous elements;
  • E previous() — returns the previous element in the list and moves the cursor position backwards;
  • int previousIndex() — returns the index of the element that would be returned by invoking previous;
  • void set(E element) — replaces the last element returned by next or previous with the specified element;
  • void add(E element) — inserts the specified element into the list immediately before the element that would be returned by next, and after the element that would be returned by previous.

Here is an example of how it works:

List<Integer> list = List.of(1, 2, 3, 4);
ListIterator<Integer> iterator = list.listIterator(); // only for lists!

// go to the last element
while (iterator.hasNext()) { iterator.next(); }

// print elements in the backward order with their indexes
while (iterator.hasPrevious()) {
    int previousIndex = iterator.previousIndex();
    int element = iterator.previous();
    System.out.println(element + " on " + previousIndex);
}

This code prints numbers in the backward order with their indexes.

4 on 3
3 on 2
2 on 1
1 on 0

If you invoke previous before previousIndex the result will differ since previous changes the state of the iterator, i.e. the current position.

Of course, if you have a simple collection without any complex iteration logic, you can easily forgo 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 your code simple and predictable. Don't be surprised if you find the Iterator pattern familiar as it is widely used, for instance, in the Collections framework, and it's likely you have already encountered it many times.

Written by

Master Java by choosing your ideal learning course

View all courses

Create a free account to access the full topic

Sign up with Google
Sign up with Google
Sign up with JetBrains
Sign up with JetBrains
Sign up with Github
Sign up with GitHub
Coding thrill starts at Hyperskill
I've been using Hyperskill for five days now, and I absolutely love it compared to other platforms. The hands-on approach, where you learn by doing and solving problems, really accelerates the learning process.
Aryan Patil
Reviewed us on