Java Iterable Interface: Iterator, ListIterator, and Spliterator

Introduction

While we can use a for or while loop to traverse through a collection of elements, an Iterator allows us to do so without worrying about index positions and even allows us to not only go through a collection, but also alter it at the same time, which isn't always possible with for loops if you're removing elements in the loop, for example.

Couple that with the ability to implement our custom Iterator to iterate through much more complex objects, as well as moving forward and backwards, and the advantages of knowing how to use it become quite clear.

This article will go fairly in-depth on how the Iterator and Iterable interfaces can be used.

Iterator()

The Iterator interface is used to iterate over the elements in a collection (List, Set, or Map). It is used to retrieve the elements one by one and perform operations over each one if need be.

Here are the methods used to traverse collections and perform operations:

  • .hasNext(): Returns true if we haven't reached the end of a collection, returns false otherwise
  • .next(): Returns the next element in a collection
  • .remove(): Removes the last element returned by the iterator from the collection
  • .forEachRemaining(): Performs the given action for each remaining element in a collection, in sequential order

First off, since iterators are meant to be used with collections, let's make a simple ArrayList with a few items:

List<String> avengers = new ArrayList<>();

// Now lets add some Avengers to the list
avengers.add("Ant-Man");
avengers.add("Black Widow");
avengers.add("Captain America");
avengers.add("Doctor Strange");

We can iterate through this list using a simple loop:

System.out.println("Simple loop example:\n");
for (int i = 0; i < avengers.size(); i++) {
    System.out.println(avengers.get(i));
}

Though, we want to explore iterators:

System.out.println("\nIterator Example:\n");

// First we make an Iterator by calling 
// the .iterator() method on the collection
Iterator<String> avengersIterator = avengers.iterator();

// And now we use .hasNext() and .next() to go through it
while (avengersIterator.hasNext()) {
    System.out.println(avengersIterator.next());
}

What happens if we want to remove an element from this ArrayList? Let's try to do so using the regular for loop:

System.out.println("Simple loop example:\n");
for (int i = 0; i < avengers.size(); i++) {
    if (avengers.get(i).equals("Doctor Strange")) {
        avengers.remove(i);
    }
    System.out.println(avengers.get(i));
}

We'd be greeted with a nasty IndexOutOfBoundsException:

Simple loop example:

Ant-Man
Black Widow
Captain America
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 3, Size: 3

This makes sense as we're altering the collection size as we're traversing it. The same goes for the advanced for loop:

System.out.println("Simple loop example:\n");
for (String avenger : avengers) {
    if (avenger.equals("Doctor Strange")) {
        avengers.remove(avenger);
    }
    System.out.println(avenger);
}

Again we're greeted with another exception:

Simple loop example:

Ant-Man
Black Widow
Captain America
Doctor Strange
Exception in thread "main" java.util.ConcurrentModificationException

This is where iterators come in handy, acting as a middle-man to remove the element from the collection but also to ensure that the traversal continues as planned:

Iterator<String> avengersIterator = avengers.iterator();
while (avengersIterator.hasNext()) {
    String avenger = avengersIterator.next();

    // First we must find the element we wish to remove
    if (avenger.equals("Ant-Man")) {
        // This will remove "Ant-Man" from the original
        // collection, in this case a List
        avengersIterator.remove();
    }
}

This is a guaranteed safe method of removing elements while traversing collections.

And to validate if the item has been removed:

// We can also use the helper method .forEachRemaining()
System.out.println("For Each Remaining Example:\n");
Iterator<String> avengersIteratorForEach = avengers.iterator();

// This will apply System.out::println to all elements in the collection
avengersIteratorForEach.forEachRemaining(System.out::println);     

And the output is:

For Each Remaining Example:

Black Widow
Captain America
Doctor Strange

As you can see, "Ant-Man" has been removed from the avengers list.

ListIterator()

ListIterator extends the Iterator interface. It is only used on Lists and it can iterate bidirectionally, meaning you can iterate front-to-back or back-to-front. It also doesn't have a current element because the cursor is always placed between 2 elements in a List, so we must use .previous() or .next() to access an element.

What's the difference between an Iterator and a ListIterator?

First, the Iterator can be applied to any collection - Lists, Maps, Queues, Sets, etc.

The ListIterator can only be applied to lists. By adding this restriction, the ListIterator can be a lot more specific when it comes to methods, and so we're introduced to a lot of new methods that help us modify lists while traversing.

If you're dealing with a List implementation (ArrayList, LinkedList, etc.), it's always preferable to use the ListIterator.

Here are the methods you'll likely use:

  • .add(E e): Inserts element into List.
  • .remove(): Removes the last element that was returned by .next() or .previous() from List.
  • .set(E e): Replaces the last element that was returned by .next() or .previous() with the specified element
  • .hasNext(): Returns true if we haven't reached the end of a List, returns false otherwise.
  • .next(): Returns the next element in a List.
  • .nextIndex(): Returns the index of the next element.
  • .hasPrevious(): Returns true if we haven't reached the beginning of a List, returns false otherwise.
  • .previous(): Returns the previous element in a List.
  • .previousIndex(): Returns the index of the previous element.

Again, let's populate an ArrayList with a few items:

ArrayList<String> defenders = new ArrayList<>();

defenders.add("Daredevil");
defenders.add("Luke Cage");
defenders.add("Jessica Jones");
defenders.add("Iron Fist");

Let's use a ListIterator to traverse a list and print out the elements:

ListIterator listIterator = defenders.listIterator(); 
  
System.out.println("Original contents of our List:\n");
while (listIterator.hasNext()) 
    System.out.print(listIterator.next() + System.lineSeparator()); 

Obviously, it works in the same fashion as the classic Iterator. The output is:

Original contents of our List: 

Daredevil
Luke Cage
Jessica Jones
Iron Fist

Now, let's try to modify some elements:

System.out.println("Modified contents of our List:\n");

// Now let's make a ListIterator and modify the elements
ListIterator defendersListIterator = defenders.listIterator();

while (defendersListIterator.hasNext()) {
    Object element = defendersListIterator.next();
    defendersListIterator.set("The Mighty Defender: " + element);
}

Printing out the list now would yield:

Modified contents of our List:

The Mighty Defender: Daredevil
The Mighty Defender: Luke Cage
The Mighty Defender: Jessica Jones
The Mighty Defender: Iron Fist

Now, let's go ahead and traverse the list backwards, as something that we can do with the ListIterator:

System.out.println("Modified List backwards:\n");
while (defendersListIterator.hasPrevious()) {
    System.out.println(defendersListIterator.previous());
}

And the output is:

Modified List backwards:

The Mighty Defender: Iron Fist
The Mighty Defender: Jessica Jones
The Mighty Defender: Luke Cage
The Mighty Defender: Daredevil

Spliterator()

The Spliterator interface is functionally the same as an Iterator. You may never need to use Spliterator directly, but let's still go over some use-cases.

You should, however, first be somewhat familiar with Java Streams and Lambda Expressions in Java.

While we will list all the methods Spliterator has, the full workings of the Spliterator interface are out of scope of this article. One thing we will cover with an example is how Spliterator can use parallelization to more efficiently traverse a Stream that we can break down.

The methods we'll use when dealing with the Spliterator are:

  • .characteristics(): Returns the characteristics that this Spliterator has as an int value. These include:
    • ORDERED
    • DISTINCT
    • SORTED
    • SIZED
    • CONCURRENT
    • IMMUTABLE
    • NONNULL
    • SUBSIZED
  • .estimateSize(): Returns an estimate of the number of elements that would be encountered by a traversal as a long value, or returns long.MAX_VALUE if it is unable to calculate.
  • .forEachRemaining(E e): Performs the given action for each remaining element in a collection, in sequential order.
  • .getComparator(): If this Spliterator's source is sorted by a Comparator, it returns that Comparator.
  • .getExactSizeIfKnown(): Returns .estimateSize() if the size is known, otherwise returns -1
  • .hasCharacteristics(int characteristics): Returns true if this Spliterator's .characteristics() contain all of the given characteristics.
  • .tryAdvance(E e): If a remaining element exists, performs the given action on it, returning true, else returns false.
  • .trySplit(): If this Spliterator can be partitioned, returns a Spliterator covering elements, that will, upon return from this method, not be covered by this Spliterator.

Like usual, let's start off with a simple ArrayList:

List<String> mutants = new ArrayList<>();

mutants.add("Professor X");
mutants.add("Magneto");
mutants.add("Storm");
mutants.add("Jean Grey");
mutants.add("Wolverine");
mutants.add("Mystique");

Now, we need to apply the Spliterator to a Stream. Thankfully, it's easy to convert between an ArrayList and a Stream due to the Collections framework:

// Obtain a Stream to the mutants List.
Stream<String> mutantStream = mutants.stream();

// Getting Spliterator object on mutantStream.
Spliterator<String> mutantList = mutantStream.spliterator();

And to showcase some of these methods, let's run each one:

// .estimateSize() method
System.out.println("Estimate size: " + mutantList.estimateSize());

// .getExactSizeIfKnown() method
System.out.println("\nExact size: " + mutantList.getExactSizeIfKnown());

System.out.println("\nContent of List:");
// .forEachRemaining() method
mutantList.forEachRemaining((n) -> System.out.println(n));

// Obtaining another Stream to the mutant List.
Spliterator<String> splitList1 = mutantStream.spliterator();

// .trySplit() method
Spliterator<String> splitList2 = splitList1.trySplit();

// If splitList1 could be split, use splitList2 first.
if (splitList2 != null) {
    System.out.println("\nOutput from splitList2:");
    splitList2.forEachRemaining((n) -> System.out.println(n));
}

// Now, use the splitList1
System.out.println("\nOutput from splitList1:");
splitList1.forEachRemaining((n) -> System.out.println(n));

And we get this as output:

Estimate size: 6

Exact size: 6

Content of List: 
Professor X
Magneto
Storm
Jean Grey
Wolverine
Mystique

Output from splitList2: 
Professor X
Magneto
Storm

Output from splitList1: 
Jean Grey
Wolverine
Mystique

Iterable()

What if for some reason we would like to make a custom Iterator interface. The first thing you should be acquainted with is this graph:

To make our custom Iterator we would need to write custom methods for .hasNext(), .next(), and .remove() .

Inside the Iterable interface, we have a method that returns an iterator for elements in a collection, that is the .iterator() method, and a method that performs an action for each element in an iterator, the .forEach() method.

For example, let's imagine we are Tony Stark, and we need to write a custom iterator to list every Iron Man suit you currently have in your armory.

First, let's make a class to get and set the suit data:

public class Suit {

    private String codename;
    private int mark;

    public Suit(String codename, int mark) {
        this.codename = codename;
        this.mark = mark;
    }

    public String getCodename() { return codename; }

    public int getMark() { return mark; }

    public void setCodename (String codename) {this.codename=codename;}

    public void setMark (int mark) {this.mark=mark;}

    public String toString() {
        return "mark: " + mark + ", codename: " + codename;
    }
}

Next, let's write our custom Iterator:

// Our custom Iterator must implement the Iterable interface
public class Armoury implements Iterable<Suit> {
    
    // Notice that we are using our own class as a data type
    private List<Suit> list = null;

    public Armoury() {
        // Fill the List with data
        list = new LinkedList<Suit>();
        list.add(new Suit("HOTROD", 22));
        list.add(new Suit("SILVER CENTURION", 33));
        list.add(new Suit("SOUTHPAW", 34));
        list.add(new Suit("HULKBUSTER 2.0", 48));
    }
    
    public Iterator<Suit> iterator() {
        return new CustomIterator<Suit>(list);
    }

    // Here we are writing our custom Iterator
    // Notice the generic class E since we do not need to specify an exact class
    public class CustomIterator<E> implements Iterator<E> {
    
        // We need an index to know if we have reached the end of the collection
        int indexPosition = 0;
        
        // We will iterate through the collection as a List
        List<E> internalList;
        public CustomIterator(List<E> internalList) {
            this.internalList = internalList;
        }

        // Since java indexes elements from 0, we need to check against indexPosition +1
        // to see if we have reached the end of the collection
        public boolean hasNext() {
            if (internalList.size() >= indexPosition +1) {
                return true;
            }
            return false;
        }

        // This is our custom .next() method
        public E next() {
            E val = internalList.get(indexPosition);

            // If for example, we were to put here "indexPosition +=2" we would skip every 
            // second element in a collection. This is a simple example but we could
            // write very complex code here to filter precisely which elements are
            // returned. 
            // Something which would be much more tedious to do with a for or while loop
            indexPosition += 1;
            return val;
        }
        // In this example we do not need a .remove() method, but it can also be 
        // written if required
    }
}

And finally the main class:

public class IronMan {

    public static void main(String[] args) {

        Armoury armoury = new Armoury();

        // Instead of manually writing .hasNext() and .next() methods to iterate through 
        // our collection we can simply use the advanced forloop
        for (Suit s : armoury) {
            System.out.println(s);
        }
    }
}

The output is:

mark: 22, codename: HOTROD
mark: 33, codename: SILVER CENTURION
mark: 34, codename: SOUTHPAW
mark: 48, codename: HULKBUSTER 2.0

Conclusion

In this article, we covered in detail how to work with iterators in Java and even wrote a custom one to explore all new possibilities of the Iterable interface.

We also touched on how Java leverages stream parallelization to internally optimize traversal through a collection using the Spliterator interface.