The difference in Iterator vs ListIterator vs Spliterator is what each of these three cursors of the Java Collections Framework can do during a loop. An Iterator walks any collection forward and can remove elements, a ListIterator moves both ways through a List and can add or replace elements, and a Spliterator splits a source into parts so that parallel streams can process them in different threads.
All three replace the legacy Enumeration from Java 1.0, which still appears in a few old APIs. The following example uses each cursor on the same list.
List<String> colors = new ArrayList<>(List.of("red", "green", "blue"));
Iterator<String> it = colors.iterator();
String first = it.next(); // "red"
it.remove(); // Iterator removes "red"
ListIterator<String> li = colors.listIterator(colors.size());
String last = li.previous(); // "blue", moving backward
li.set("navy"); // ListIterator replaces "blue"
Spliterator<String> sp = colors.spliterator();
Spliterator<String> half = sp.trySplit();
long splitOff = half.estimateSize(); // 1, Spliterator hands "green" to a second part
long remaining = sp.estimateSize(); // 1
String result = String.join(",", colors); // "green,navy"
Notice that only the Spliterator can split and only the ListIterator can move backward. We compare the four cursors in one table, walk through each with its methods and an example, and finish with performance advice, best practices and the questions developers ask most.
1. Iterator vs ListIterator vs Spliterator vs Enumeration
Apart from using for loops, every collection in the Java Collections Framework gives us cursor objects for traversal and for changing the data during the loop. The cursors differ in where they apply, which operations they allow and how they react when the collection changes underneath them.

Only the ListIterator changes direction and only the Spliterator splits, and those two capabilities decide most choices between the cursors.
| Feature | Enumeration | Iterator | ListIterator | Spliterator |
|---|---|---|---|---|
| Applies to | Legacy classes such as Vector, Hashtable and Stack, and a few old APIs | Any Collection, plus Stream.iterator() | Only List implementations such as ArrayList and LinkedList | Any Collection, arrays and streams |
| Can perform | Only read operations | Read and remove | Read, add, remove and replace | Only read operations |
| Can traverse in | Only forward direction | Only forward direction | Both forward and backward directions | Only forward direction |
| Processing | Sequential | Sequential | Sequential | Sequential, plus splitting for parallel work |
| Changes during the loop | Not detected, elements can be skipped | Fail-fast in general-purpose collections | Fail-fast in general-purpose collections | Fail-fast after binding, late-binding in most collections |
| Iteration style | External | External, with forEachRemaining() for bulk | External | One at a time with tryAdvance(), or bulk with forEachRemaining() |
| Thread safety | Not thread-safe as a cursor | Not thread-safe | Not thread-safe | Not thread-safe, split first and use one part per thread |
| Introduced in | Java 1.0 | Java 1.2 | Java 1.2 | Java 8 |
Fail-fast means that the cursor throws ConcurrentModificationException when the collection changes structurally by any code other than the cursor itself. The concurrent collections, such as ConcurrentHashMap and CopyOnWriteArrayList, return weakly consistent or snapshot cursors instead, which never throw that exception.
2. Java Enumeration for Iteration over Legacy Classes
The java.util.Enumeration interface is the legacy cursor available since JDK 1.0, used to iterate over the elements of Vector and the keys or values of Hashtable. It has two methods, hasMoreElements() and nextElement(), and no way to remove an element.
Vector<Integer> vector = new Vector<>(List.of(1, 2, 3));
Enumeration<Integer> enumeration = vector.elements();
StringJoiner seen = new StringJoiner(" ");
while (enumeration.hasMoreElements()) {
seen.add(String.valueOf(enumeration.nextElement()));
}
String printed = seen.toString(); // "1 2 3"
Iterator<Integer> modern = vector.elements().asIterator();
Integer firstValue = modern.next(); // 1
New code uses Enumeration only when an old API returns one, such as ClassLoader.getResources(), and converts it right away with asIterator() from Java 9 or Collections.list(). Its disadvantages are the reason for that rule.
- It works only with legacy classes and a few old APIs, so it is not a universal cursor.
- It supports only read operations and only the forward direction.
- It does not detect changes during the loop, so a removal from the Vector can make it skip an element without any error.
3. Java Iterator for Simple Iteration
To overcome the limits of Enumeration, Java 1.2 introduced the java.util.Iterator interface together with the Collections Framework. Every collection class returns one from iterator(), so it is a universal cursor, and the enhanced for loop uses it behind the scenes.
// does not compile: method signatures of the java.util.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();
// Performs the given action for each remaining element (since Java 8).
default void forEachRemaining(Consumer<? super E> action);
We use an Iterator to loop over any collection when the loop may remove elements. The following example removes every multiple of 5 from a set of scores while it loops.
Set<Integer> scores = new TreeSet<>(Set.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
Iterator<Integer> iterator = scores.iterator();
while (iterator.hasNext()) {
if (iterator.next() % 5 == 0) {
iterator.remove();
}
}
String left = scores.toString(); // "[1, 2, 3, 4, 6, 7, 8, 9]"
When the condition fits in a lambda, scores.removeIf(s -> s % 5 == 0) does the same in one line. The explicit Iterator still wins when the loop does more per element, such as logging what it removes.
- It works with every collection, including sets, queues and map views.
- It supports removal during the loop, which no for-each loop allows.
- It moves only forward and cannot add or replace elements.
4. Java ListIterator for Bi-directional Iteration on Lists
The ListIterator interface extends Iterator for lists. It moves in both directions, reports the index of the next and previous element, and can add, remove and replace elements during the loop.
// does not compile: method signatures of the java.util.ListIterator interface
// For iteration in forward direction
boolean hasNext();
E next();
int nextIndex();
// For iteration in backward direction
boolean hasPrevious();
E previous();
int previousIndex();
// For operations on elements
void add(E e); // inserts at the cursor
void set(E e); // replaces the last element returned by next() or previous()
void remove(); // removes the last element returned by next() or previous()
The following example removes one element, replaces another and appends a new one, all in a single pass over a mutable list.
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5));
ListIterator<Integer> iterator = numbers.listIterator();
while (iterator.hasNext()) {
int 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 at the end
String result = numbers.toString(); // "[2, 3, 4, 50, 6]"
The ListIterator is the most capable cursor, but only lists provide one.
- It supports read, add, remove and replace operations in both directions.
- It works only with List implementations.
- Its add() and remove() calls shift elements in an ArrayList, which makes them O(n), whereas a LinkedList does them in O(1).
5. Java Spliterator for Parallel Iteration over Large Collections
Java 8 introduced the java.util.Spliterator interface for streams. It traverses elements like an Iterator, and it can also split off part of its elements into a second Spliterator, so that two threads can process the two parts at the same time. We rarely call it ourselves, because stream() and parallelStream() use it internally.
// does not compile: method signatures of the java.util.Spliterator interface
// Returns an estimate of the remaining elements, or Long.MAX_VALUE if unknown.
long estimateSize();
// If a remaining element exists, performs the action on it and returns true.
boolean tryAdvance(Consumer<? super T> action);
// Performs the action for each remaining element, sequentially in the current thread.
default void forEachRemaining(Consumer<? super T> action);
// Splits off some elements into a new Spliterator, or returns null.
Spliterator<T> trySplit();
The following example splits a list of 30,000 strings into two halves and counts the elements of each half in its own thread. Each thread gets its own Spliterator, because a single instance is not thread-safe.
List<String> bigList = Stream.generate(() -> "Hello").limit(30_000).toList();
Spliterator<String> split = bigList.spliterator();
long before = split.estimateSize(); // 30000
Spliterator<String> split1 = split.trySplit();
long secondHalf = split.estimateSize(); // 15000
long firstHalf = split1.estimateSize(); // 15000
LongAdder processed = new LongAdder();
try (ExecutorService pool = Executors.newFixedThreadPool(2)) {
pool.submit(() -> split.forEachRemaining(s -> processed.increment()));
pool.submit(() -> split1.forEachRemaining(s -> processed.increment()));
}
long done = processed.sum(); // 30000
The try-with-resources block waits for both tasks before it closes the executor, so the count is complete when we read it. A parallel stream does the same splitting and thread handling for us, as in bigList.parallelStream().count().
- It splits work for parallel processing of large collections.
- It works with any collection, arrays and streams, and reports the size and characteristics of its source.
- It supports only read operations and only the forward direction.
6. Performance and Best Practices
The cursors themselves cost about the same per element. The difference comes from the collection behind them and from what the loop does, so the choice is mostly about the operations we need.
6.1. Use Cases and Performance
Each cursor fits a different job, and the collection type matters more than the cursor for speed.
- The Iterator and the ListIterator traverse at the same speed. Pick the ListIterator only when the loop moves backward, needs the index, or adds and replaces elements.
- On a LinkedList, a cursor is the fast way to traverse, whereas an index-based loop with get(i) walks the nodes again for every element.
- The Spliterator is faster for large, SIZED sources such as ArrayList and arrays with CPU-heavy work per element. Splitting a small list costs more than it saves.
- The Enumeration has no performance advantage. Its nextElement() call on a Vector takes a lock for every element, which is extra work that an ArrayList iterator does not do.
6.2. General Best Practices
In general, the enhanced for-each loop is the best choice for reading a collection, and it uses the Iterator internally. The ways to iterate a collection article compares it with the other loops.
List<String> names = List.of("red", "green");
StringJoiner joined = new StringJoiner("+");
for (String name : names) {
joined.add(name);
}
String both = joined.toString(); // "red+green"
- Use removeIf() for conditional removal, and an explicit Iterator only when the loop needs more than a condition.
- Use list.reversed() from Java 21 to read a list backward, and keep the ListIterator for loops that edit the list.
- Use stream() for filtering and mapping, and parallelStream() only after measuring that it is faster for the data at hand.
- Convert an Enumeration to an Iterator at the API boundary and keep the rest of the code on modern types.
7. Java Cursor FAQs
Developers ask these questions when they compare the Java cursors.
7.1. How Is a For-each Loop Different from an Iterator?
The for-each loop uses an Iterator internally, so both have the same performance and visit elements in the same order.
There are three practical differences between them.
In a for-each loop, modifying the collection structurally throws ConcurrentModificationException, whereas the explicit Iterator can remove elements with remove(). Calling next() past the last element throws NoSuchElementException, which a for-each loop never does. A for-each loop also works on arrays, whereas an Iterator comes only from an Iterable.
7.2. How Is ListIterator Different from Iterator?
A ListIterator moves in both directions, knows the index of the next and previous element and can add and replace elements, but it works only with lists. An Iterator moves forward and can only remove, but it works with every collection.
7.3. How Do We Find the Index of the Current Element in a ListIterator?
A ListIterator has no current element. Its cursor always lies between the element that previous() would return and the element that next() would return, so a list of length n has n + 1 cursor positions. The nextIndex() and previousIndex() methods give the indexes on both sides, and calling next() and previous() one after the other returns the same element.
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5));
ListIterator<Integer> iterator = numbers.listIterator();
Integer a = iterator.next(); // 1
Integer b = iterator.next(); // 2
int index = iterator.previousIndex(); // 1
Integer c = iterator.previous(); // 2
Integer d = iterator.next(); // 2
7.4. How can we estimate the size of the remaining elements in a Spliterator?
Call estimateSize(). It returns the number of elements that a forEachRemaining() call would still process, or Long.MAX_VALUE if the size is infinite, unknown or too expensive to compute. The getExactSizeIfKnown() method returns the same value for a SIZED source and -1 otherwise.
List<String> list = Stream.generate(() -> "Hello").limit(10).toList();
Spliterator<String> split = list.spliterator();
boolean r1 = split.tryAdvance(s -> {}); // true
boolean r2 = split.tryAdvance(s -> {}); // true
boolean r3 = split.tryAdvance(s -> {}); // true
long left = split.estimateSize(); // 7
7.5. What is fail-fast behavior, and how does it relate to Iterator and ListIterator?
A fail-fast cursor throws ConcurrentModificationException as soon as it notices that the collection changed structurally by code other than the cursor itself. The Iterator, ListIterator and Spliterator of general-purpose collections such as ArrayList and HashMap are fail-fast. They compare a modification counter named modCount with the value they saved when they were created.
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5));
ListIterator<Integer> iterator = numbers.listIterator();
Integer firstNumber = iterator.next(); // 1
numbers.add(0, 10); // changes the list outside the iterator
Integer second = iterator.next(); // ConcurrentModificationException
The Enumeration of Vector is not fail-fast, and it is not fail-safe either, because it reads the live array and can skip elements after a removal. The cursors of concurrent collections are weakly consistent or work on a snapshot, so they never throw ConcurrentModificationException.
7.6. Which cursor should we use for parallel processing?
Use a Spliterator, in most code through parallelStream(), which splits the source and runs the parts on the common ForkJoinPool. An Iterator or ListIterator can only be consumed by one thread from start to end.
8. Conclusion
The Iterator is the universal cursor for reading and removing, the ListIterator adds backward movement, indexes and in-place edits for lists, and the Spliterator adds splitting and size information for parallel streams. All three are fail-fast in general-purpose collections and none of them is thread-safe.
The legacy Enumeration survives only in old APIs, so we convert it with asIterator(). For everyday code, the for-each loop, removeIf(), reversed() and streams cover most needs, and the cursors are there for the cases they do not cover.
9. References
- Java SE 25 API, Iterator
- Java SE 25 API, ListIterator
- Java SE 25 API, Spliterator
- Java SE 25 API, Enumeration
Happy Learning !!