Java Iteration Interface: Iterator, ListIterator, and pliterator

Posted by trochia on Tue, 27 Aug 2019 02:42:31 +0200

1. Introduction

When we use for or while loops to traverse the elements of a collection, Iterator allows us not to worry about the location of the index, or even let us not only traverse a collection, but also change it.For example, if you want to delete elements in a loop, the for loop may not always work.

With custom iterators, we can iterate over more complex objects and move forward and backward, and it will become clear how to take advantage of them.

This article will discuss in depth how to use the Iterator and Iterable interfaces.

2. Iterator()

The Iterator interface is used to iterate over elements (List, Set, or Map) in a collection.It retrieves elements one by one and operates on each element as needed.

The following are the methods used to traverse collections and perform operations:

  • .hasNext(): Returns true if the end of the collection has not been reached, false otherwise
  • .next(): Returns the next element in the collection
  • .remove(): Remove the last element returned by the iterator from the set
  • .forEachRemaining(): Performs the given operation in order for each remaining element in the collection

First, since iterators are for collections, let's make a simple ArrayList with a few elements:

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

However, 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 delete an element from this ArrayList?Let's try 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'll get 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 to change the size of a collection as it traverses, as does an enhanced 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);
}

We received another exception again:

Simple loop example:

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

Iterators come in handy, acting as intermediaries, removing elements from the collection, and ensuring that 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 safe way to ensure that elements are deleted when traversing a collection.

And confirm that the element has been deleted:

// 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);

The output is as follows:

For Each Remaining Example:

Black Widow
Captain America
Doctor Strange

As you can see, the Ants have been removed from the Avengers Alliance list.

2.1. ListIterator()

ListIterator inherits from the Iterator interface.It is only used on Lists and can iterate in both directions, which means you can iterate from front to back or from back to front.It also does not have a current element, because the cursor is always placed between two elements of the List, so we use.previous() or.next() to access the element.

What is the difference between Iterator and ListIterator?

First, Iterator can be used for any collection -- List, Map, Queue, Set, and so on.

ListIterator can only be applied to Lists. By adding this restriction, ListIterator can be more specific in terms of methods, so we have introduced a number of new methods that can help us modify it as we traverse.

If you are working with List implementations (ArrayList, LinkedList, and so on), it is preferable to use ListIterator.

Here's what you might use:

  • .add (E): Add elements to the List.
  • .remove(): Remove the last element returned by.next() or.previous() from the List.
  • .set(E): Overrides the last element returned by List.next() or.previous() with the specified element.
  • .hasNext(): Returns true if the end of the List has not been reached, otherwise returns false.
  • .next(): Returns the next element in the List.
  • .nextIndex(): Returns the subscript of the next element.
  • .hasPrevious(): Returns true if the beginning of the List has not been reached, otherwise returns false.
  • .previous(): Returns the previous element of the List.
  • .previousIndex(): Returns the subscript of the previous element.

Again, let's make an ArrayList of elements:

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

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

Let's use ListIterator to iterate through a List and print its 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 the same way as Iterator.The output is as follows:

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

Now printing the List yields the following results:

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 iterate through the list upside down, as we can with ListIterator:

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

The output is as follows:

Modified List backwards:

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

3. Spliterator()

The Spliterator interface is functionally the same as Iterator.You may never need to use Spliterator directly, but let's continue with some use cases.

But first you should be familiar with Java Streams and Lambda Expressions in Java.

Although we will list all methods owned by Spliterator, the entire work of the Spliterator interface is beyond the scope of this article.We will use an example to discuss how Spliterator can use parallelism to traverse treams that we can decompose more effectively.

The way we work with Spliterator is:

  • .characteristics()

    : Returns what the Spliterator has as

    int

    The characteristics of the values.These include:

    • ORDERED
    • DISTINCT
    • SORTED
    • SIZED
    • CONCURRENT
    • IMMUTABLE
    • NONNULL
    • SUBSIZED
  • .estimateSize(): Returns an estimate of the number of elements traversed as long values, or long.MAX_VALUE if it cannot be returned.

  • .forEachRemaining (E): Performs a given operation on each remaining element in the collection in order.

  • .getComparator(): If the source of the Spliterator is sorted by Comparator, it returns Comparator.

  • .getExactSizeIfKnown(): Returns.estimateSize() if the size is known, or -1 if it is not.

  • .hasCharacteristics(int characteristics): Returns true if the.characteristics() of this Spliterator contains all the given features.

  • .tryAdvance (E): If there are remaining elements, perform the given operation on them, return true, otherwise return false.

  • .trySplit(): If this Spliterator can be partitioned, return a Spliterator override element, when returned from this method, it will not be overridden by this Spliterator.

As always, let's start 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 Spliterator to Stream.Fortunately, the Collections framework makes it easy to convert between ArrayList and Stream:

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

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

To show some of these methods, let's run them separately:

// .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));

We will get the 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

4. Iterable()

What if for some reason we want to create a custom Iterator interface?The first thing you need to be familiar with is this picture:

To create a custom Iterator, we need to make a custom implementation for.hasNext(),.next(), and.remove().

In the Iterator interface, there is a method that returns an iterator of elements in a collection, the.iterator() method, and a method that performs an operation for each element in the iterator, the.dorEach() method.

For example, suppose we're Tony Stark, we need to write a custom iterator to list each Iron Man suit in our current arsenal.

First, let's create a class to get and set 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 a 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
    }
}

Finally, the main method 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 as follows:

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

5. Summary

In this article, we discussed in detail how to use iterators in Java, and even wrote a custom iterator to explore all the new possibilities of the Iterable interface.

We also discussed how Java leverages Stream's parallelism and uses the Spliterator interface to internally optimize traversal of collections.

August welfare arrived on time, pay attention to the public number

Background reply: 003 will receive the July translation collection oh~

Futures benefits reply: 001, 002 can be received!

Topics: Java Lambda