Java Enumeration, Iterator, ListIterator and Spliterator

In Java, all types of enumerations and iterators (such as Iterator, ListIterator, SplitIterator) are simply navigational cursors and the main purpose of these cursors is to iterate over the elements of the collection. Each cursor has its own features, advantages and disadvantages.

In this article, we will walk through these iterators, important methods, advantages, and disadvantages with examples. We will also talk about the performance of the different cursors in the different situations, and best practices also.

1. Different Types of Iterators for Collections

Apart from using for-loops, collection types allow Iterator implementations for clean code, better performance and data manipulation during iterations.

The following table summarizes the primary differences between the legacy enumeration and various iterators.

Feature(s)EnumerationIteratorListIteratorSpliterator
Applies toOnly legacy classes like Vector, HashTable and StackAny collection object, and Java 8 streams as it’s a universal cursorOnly List implementations like ArrayList, LinkedList, Stack etcAny collection object, arrays and streams
Can performOnly read operationsRead as well as remove operationsRead, add, remove, and update operationsOnly read operations, but also supports concurrent read
Can Traverse inOnly forward directionOnly forward directionBoth forward and backward directionsOnly forward direction
ProcessingSerialSerialSerialSerial and Parallel
Fail-Safe/FastFail-safeFail-fastFail-fastFail-fast
UsesExternal IterationExternal IterationExternal IterationInternal Iteration
Thread SafetyThread-SafeNot Thread-SafeNot Thread-SafePotentially Thread-Safe
Introduced inJava 1.0Java 1.2Java 1.2Java 1.8

2. Java Enumeration for Iteration over Legacy Classes

The java.util.Enumeration interface is the legacy interface available since JDK1.0, used to iterate over the elements of the legacy collection classes.

There are two important methods of this Enumeration interface.

//Tests if this enumeration contains more elements.
boolean hasMoreElements(); 

//Returns the next element of this enumeration if this enumeration object has at least one more element to provide.
E nextElement(); 

2.1. When to Use Enumeration

We can use Enumeration when we want to iterate over the elements of any legacy collection classes in a fail-safe manner.

2.2. Enumeration Example

We can get the Enumeration object by calling the .elements() method on the collection class object.

Vector<Integer> vector = ...;

Enumeration<Integer> enumeration = vector.elements();

while(enumeration.hasMoreElements()){

  System.out.print(enumeration.nextElement() + " ");
}

2.3. Advantages and Disadvantages

Advantages

  • It is a fail-safe iterator, hence in the case of iterating a collection using the Enumeration, we will never get any exceptions if a collection is structurally modified while iterating over it.

Disadvantages

  • We can use Enumeration, only on legacy collection classes, hence it is not a universal cursor.
  • Using the Enumeration, we can only perform the read operations.
  • Using the Enumeration, we can only move in the forward direction.
  • It can be slow in the case of very large collections.

3. Java Iterator for Simple Iteration

To overcome some of the above disadvantages of the Enumeration, Java introduced the java.util.Iterator interface in JDK1.2. Iterator can be used to iterate over the elements of any collection classes present in Java, hence it is a universal cursor. Iterating using Iterator we can perform both read and remove operations on the elements of the collection.

There are three important methods of this Iterator interface.

//Returns true if the iteration has more elements.
boolean hasNext(); 

//Returns the next element in the iteration.
E next(); 

//Removes from the underlying collection the last element returned by this iterator.
default void remove(); 

3.1. When to Use Iterator?

We can use the Iterator to iterate over the elements of any of the collection classes with the support of removing any element from the collection.

3.2. Iterator Example

The following program removes the element 5 from the mutableset and print the remaining elements.

Set<Integer> immutableSet = Set.of(1,2,3,4,5,6,7,8,9,10); // Immutable Set
Set<Integer> mutableset = new HashSet<>(immutableSet);  // Mutable Set

Iterator<Integer> iterator = mutableset.iterator();

int num;

while (iterator.hasNext()){

    if ((num = iterator.next()) == 5) { 
      iterator.remove(); 
    }
    else { 
      System.out.print(num + " "); // # Prints '1 2 3 4 6 7 8 9 10'
    } 
}

3.3. Advantages and Disadvantages

Advantages

  • As it’s a universal cursor we can use Iterator on any of the collection classes.
  • The Iterator supports both read and remove operations while iterating over elements of any collection.

Disadvantages

  • Using the Iterator, we can only perform the read and remove operations, not add, and update operations.
  • Using the Iterator, we can only move in the forward direction.
  • It is a fail-fast iterator.
  • It can be slow in the case of very large collections.

4. Java ListIterator for Bi-directional Iteration on Lists

ListIterator can be used to iterate in forward as well as backward directions over the elements of any List-implemented collection classes. Using ListIterator we can perform read, remove, add, and update operations while iterating over the elements of the collection.

The ListIterator interface extends the Iterator interface, hence all methods present in the Iterator interface are by default available to the ListIterator interface.

There are nine important methods of this ListIterator interface.

// For iteration in forward direction

boolean hasNext(); 
E next();  //Returns the next element in the list and advances the cursor position.
int nextIndex(); //Returns the index of the element that would be returned by a subsequent call to next();
 
// For iteration in backward direction

boolean hasPrevious(); 
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 a subsequent call to previous();

// For operations on elements

void add(E e); //Inserts the specified element into the list.
void set(E e); //Replaces the last element returned by next() or previous() with the specified element.
void remove(); //Removes from the list the last element that was returned by next() or previous().

4.1. When to Use ListIterator?

We can use the ListIterator to iterate in forward as well as backward direction over the elements of List implemented collection classes with the support of adding, updating, and removing any element from the collection.

4.2. ListIterator Example

The following program demonstrates various operations on a mutable list. The program uses a while loop to iterate through the numbers list using the iterator.

List<Integer> immutableList = List.of(1, 2, 3, 4, 5);   // Immutable List
List<Integer> numbers = new ArrayList<>(immutableList); // Mutable List

ListIterator<Integer> iterator = numbers.listIterator();

int num;
while (iterator.hasNext()) {
  num = iterator.next();
  if (num == 1) { 
    iterator.remove();   //Removes '1'
  } else if (num == 5) { 
    iterator.set(50);   //Changes '5' to '50'
  }
}

iterator.add(6);  //Adds '6'

System.out.println(numbers); // [2, 3, 4, 50, 6]

4.3. Advantages and Disadvantages

Advantages

  • The ListIterator is the most powerful cursor as it supports all read, add, remove, and update operations.
  • Using ListIterator, we can iterate in both forward and backward directions.

Disadvantages

  • We can use ListIterator, only on List implemented collection classes.
  • It is a fail-fast iterator.
  • It can be slow in the case of very large collections.

5. Java Spliterator for Parallel Iteration over Large Collections

In all the above iterators one common disadvantage is there, that they are slow in the case of very large collections. To solve this problem, Java introduced the java.util.Spliterator interface in the JDK1.8.

The Spliterator interface is an internal iterator that breaks the stream into smaller parts. These smaller parts can be processed in parallel. Very few times we use the Spliterator directly in our program, we use it indirectly by calling the stream(), and parallelStream() methods which internally use the Spliterator.

See also : Java Spliterator interface

There are four important methods of this Spliterator interface.

// Returns an estimate of the number of elements that would be encountered by a forEachRemaining traversal, or returns Long.MAX_VALUE if infinite, unknown, or too expensive to compute.
long estimateSize(); 

// If a remaining element exists, performs the given action on it, returning true; else returns false.
boolean tryAdvance(Consumer<? super T> action); 

// Performs the given action for each remaining element, sequentially in the current thread, until all elements have been processed or the action throws an exception.
default void forEachRemaining(Consumer<? super T> action); 

//  if the spliterator can be partitioned, returns a Spliterator covering elements, that will, upon return from this method, not be covered by this Spliterator.
Spliterator<T> trySplit();

5.1. When to Use Spliterator?

We can use the Spliterator to iterate and process the elements from a very large collection, by dividing the collection into multiple Spliterator objects and processing them parallelly in different threads.

5.2. Spliterator Example

The following program demonstrates the use of Spliterator and multi-threading to process a large list of strings. The bigList is created using a Java 9+ feature called Stream.generate(). Then it splits the bigList into multiple parts for parallel processing using the trySplit() method. It creates a new Spliterator with name split1.

Finally, we create two threads that process both halves of the list and print each element. Note that both threads run concurrently, processing their respective halves of the list in parallel.

List<String> bigList = Stream.generate(() -> "Hello").limit(30000).toList();

Spliterator<String> split = bigList.spliterator(); 
System.out.println(split.estimateSize()); // 30000

Spliterator<String> split1 = split.trySplit();

System.out.println(split.estimateSize()); // 15000
System.out.println(split1.estimateSize()); // 15000

new Thread(() -> split.forEachRemaining(elem -> System.out.println("TH1 " + elem))).start();
new Thread(() -> split1.forEachRemaining(elem -> System.out.println("TH2 " + elem))).start();

5.3. Advantages and Disadvantages

Advantages

  • As it supports parallel processing, we can process very large collections into different threads.
  • We can use Spliterator on any collection object, arrays, and streams.
  • We can know the size in advance.

Disadvantages

  • Using the Spliterator, we can only perform the read operations, and can’t modify the collection structurally.
  • Using the Spliterator, we can only move in the forward direction.
  • It is a fail-fast iterator.

6. Performance and Best Practices

6.1. Use Cases And Performance

  • In the case of legacy collection classes, the Enumeration provides the best performance as it is the fail-safe iterator.
  • The Iterator and the ListIterator both provide the same performance, but if our use case is to iterate in both forward and backward directions, and also want additional operations support like add and update then the ListIterator fits the best.
  • If our use case is to parallelly process the elements of the very large collection in different threads, then Spliterator fits the best. But keep in mind while using the Spliterator perform only thread-safe operations to avoid race conditions.

6.2. General Best Practices

In general, if the size of the collection is not so large, then it’s the best practice to use the enhanced for-each loop, which internally uses the Iterator.

for (T element : collection) { 
  //... 
}

In the case of the collection being large in size, using the .stream() and the .parallelStream() from the Stream API is the best practice, as the Streams are lazily operated. Although Stream internally uses the Spliterator only.

big.stream().forEach(System.out::println);

7. FAQs

7.1. Differences between For-each and Iterator

The for-each loop internally uses the iterator to iterate over the elements of the collection. Hence, the performance of both for-each loop and iterator is the same.

  • In the case of iterating over the collection elements using the for-each loop, if we try to modify the collection structurally, we will get the ConcurrentModificationException. But in the case of the iterator, we can modify the collection using the method provided by the iterator.
  • In the case of iterating over the collection elements using the iterator, if we try to get the next element (.next()) without checking whether the next element exists (.hasNext()) or not we will get NoSuchElementException if there is no next element. But in the case of the for-each loop, there is no such case.
  • We can use the for-each loop on both collection and array, and we can use the iterator on collections and streams.

7.2. Differences between Iterator and ListIterator

IteratorListIterator
We can traverse only in the forward directionWe can traverse in both forward and backward direction
It’s a universal cursor, hence it can be applicable to all collection classesIt’s only applicable to List implemented collection classes
Using it we can only perform read and remove operationsUsing it we can perform read, add, remove, and update operations, hence it’s the most powerful cursor
There is no way to get any information related to the index of the elementWe can get the index of the next element using nextIndex() method, and index of the previous element using previousIndex() method

7.3. How to find the Index of the Current Element pointed by ListIterator?

ListIterator‘s cursor doesn’t point to an element directly.

According to Java Docs,

ListIterator has no current element; its cursor position always lies between the element that would be returned by a call to previous() and the element that would be returned by a call to next(). An iterator for a list of length n has n+1 possible cursor positions

                      Element(0)   Element(1)   Element(2)   ... Element(n-1)
 cursor positions:  ^            ^            ^            ^                  ^ 

Hence, the alternating calls to the next() and previous() will return the same element repeatedly.

List<Integer> numbers = new ArrayList<>(List.of(1,2,3,4,5));

ListIterator<Integer> iterator = numbers.listIterator();
System.out.println(iterator.next()); //1

System.out.println(iterator.next()); //2
System.out.println(iterator.previous()); //2

System.out.println(iterator.next()); //2
System.out.println(iterator.previous()); //2

7.4. How can we estimate the size of the remaining elements in a Spliterator?

To know the estimated size of the remaining elements in a Spliterator, the Spliterator interface defines one method named estimateSize(). It returns an estimate of the number of elements that would be encountered by a forEachRemaining() traversal.

long estimateSize(); //returns the estimated size, or Long.MAX_VALUE if infinite, unknown, or too expensive to compute.
List<String> list = Stream.generate(() -> "Hello").limit(10).toList(); //10 elements

Spliterator<String> split = list.spliterator();

split.tryAdvance(System.out::println); // Hello
split.tryAdvance(System.out::println); // Hello
split.tryAdvance(System.out::println); // Hello

System.out.println(split.estimateSize()); // 7

7.5. What is fail-fast behavior, and how does it relate to Iterator and ListIterator?

While iterating over the elements of the collection, if the collection is structurally modified using the collection object, then while iterating the elements, the cursor immediately throws the ConcurrentModificationException, this behavior of any cursor is known as fail-fast behavior. Except for Enumeration, all the other cursors (Iterator, ListIterator, and Spliterator) are fail-fast in nature.

The fail-fast iterators internally maintain a flag named modCount, and call the checkForComodification() method to check whether the collection is modified or not!

try {
  List<Integer> numbers = new ArrayList<>(List.of(1,2,3,4,5));
  ListIterator<Integer> iterator = numbers.listIterator();

  System.out.println(iterator.next()); // 1

  numbers.add(0, 10); //Modifies the collection. This should throw error !
  System.out.println(iterator.next());
} 
catch (ConcurrentModificationException exception) {
  System.out.println("Attempted to the modify collection"); // Attempted to the modify collection
  System.out.println(exception); // java.util.ConcurrentModificationException
}

8. Conclusion

In this tutorial, we learned the differences between Enumeration, Iterator, ListIterator, and Spliterator with their important methods and an example. We also learned some miscellaneous topics like the difference between a for-each loop and an iterator, an element pointed by the listiterator, estimating the size of remaining elements in a spliterator, and the fail-fast behavior of the iterator.

Happy Learning !!

Source Code on Github

Comments

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.

Our Blogs

REST API Tutorial

Dark Mode

Dark Mode