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()
: Returnstrue
if we haven't reached the end of a collection, returnsfalse
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 List
s 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 aListIterator
?
First, the Iterator
can be applied to any collection - List
s, Map
s, Queue
s, Set
s, 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()
: Returnstrue
if we haven't reached the end of a List, returnsfalse
otherwise..next()
: Returns the next element in a List..nextIndex()
: Returns the index of the next element..hasPrevious()
: Returnstrue
if we haven't reached the beginning of a List, returnsfalse
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());
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
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 anint
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 along
value, or returnslong.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 thisSpliterator
's source is sorted by aComparator
, it returns thatComparator
..getExactSizeIfKnown()
: Returns.estimateSize()
if the size is known, otherwise returns-1
.hasCharacteristics(int characteristics)
: Returnstrue
if thisSpliterator
's.characteristics()
contain all of the given characteristics..tryAdvance(E e)
: If a remaining element exists, performs the given action on it, returningtrue
, else returnsfalse
..trySplit()
: If thisSpliterator
can be partitioned, returns aSpliterator
covering elements, that will, upon return from this method, not be covered by thisSpliterator
.
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.