Iterators and Iterable
A complete guide to Java's iteration model: the Iterable and Iterator interfaces, the for-each loop, ListIterator for bidirectional traversal, building custom iterable classes, safe removal during iteration, and Java 8's Spliterator.
How Java Thinks About Iteration
Before looking at the specific interfaces, it helps to understand the problem they solve. You often have a collection of things and you need to process each one in sequence. A raw index-based loop works perfectly well for arrays and lists, but it breaks down for structures that have no meaningful index: a Set has no positions, a LinkedList has positions but accessing each one by index is expensive, and a custom data structure like a tree or a graph may not expose positions at all.
Java's design for this is the Iterator pattern. A class that holds elements produces a small helper object, the iterator, that knows how to move through those elements one by one. The iterator hides every detail of the internal structure. The code that consumes elements does not need to know whether the underlying storage is an array, a linked list, a hash table, or something else entirely. It only knows that the iterator can answer two questions: "is there another element?" and "give me the next one."
The Iterable interface sits one level above this. It is the contract that a class uses to announce it can produce an iterator. The for-each loop is Java's syntactic shortcut for the pattern: instead of getting the iterator yourself, starting the loop manually, and calling the right methods, you write a clean for (Element e : collection) statement and the compiler handles the expansion.
The Iterable Interface and the for-each Loop
java.lang.Iterable<T> is one of the simplest interfaces in the Java library. It has exactly one abstract method: Iterator<T> iterator(). Any class that implements this interface can participate in a for-each loop. Every collection class in java.util implements it, which is why iterating over a List, a Set, or a Queue all use exactly the same syntax.
The for-each loop is a compiler transformation. When the compiler sees for (String s : list), it expands it into a call to list.iterator() followed by a while loop that calls hasNext() and next() on the resulting iterator. The transformation is exact and predictable, which means understanding the generated code helps you understand both why the loop works and why certain operations, such as removing elements while iterating, can go wrong if done incorrectly.
What the compiler generates from a for-each loop
The two forms below are completely equivalent. The for-each version is simply a cleaner way to write the explicit iterator version.
- For-each (what you write):for (String s : list) { System.out.println(s); }
- Expanded (what the compiler generates):Iterator
it = list.iterator(); while (it.hasNext()) { String s = it.next(); System.out.println(s); } - Key implication:The iterator variable is hidden in the for-each form. You cannot call it.remove() without switching to the explicit form.
Iterable and the for-each Expansion
JavaShowing the for-each loop alongside its explicit iterator equivalent, and why they behave identically.
The Iterator Interface: hasNext(), next(), remove()
java.util.Iterator<T> defines the three operations that make traversal possible. The first two are straightforward. hasNext() returns true if there is at least one more element left to visit. next() returns the current element and advances the cursor to the next position. Calling next() when hasNext() is false throws a NoSuchElementException, so always check before advancing.
The third method, remove(), is the correct and only safe way to remove an element from a collection while iterating over it. It removes the element most recently returned by next(). This matters because the iterator knows exactly where the cursor is sitting inside the collection and can adjust its internal state accordingly. Calling collection.remove() directly while an iterator is active bypasses this coordination, changes the collection's structure without the iterator's knowledge, and causes the iterator to throw a ConcurrentModificationException the next time you call hasNext() or next().
The remove() method also has a state requirement: you must call next() at least once before calling remove(), and you can only call remove() once per call to next(). Violating either rule throws an IllegalStateException.
Rules for safe Iterator use
Breaking these rules produces runtime exceptions. The compiler cannot catch them, so understanding the contract is essential.
- Always call hasNext() before next():next() throws NoSuchElementException if there are no more elements. hasNext() is your guard.
- Use iterator.remove() not collection.remove():Direct removal from the collection while an iterator is active triggers ConcurrentModificationException.
- Call next() before each remove():remove() operates on the element most recently returned by next(). Calling it without a prior next() throws IllegalStateException.
- Only one remove() per next() call:Calling remove() twice in a row without an intervening next() throws IllegalStateException.
Iterator: hasNext(), next(), remove()
JavaSafe traversal, correct in-place removal, and the ConcurrentModificationException trap demonstrated side by side.
ListIterator: Bidirectional Traversal
The standard Iterator only moves forward. Once you have called next() and moved past an element, there is no going back. For lists specifically, Java provides ListIterator<T>, which extends Iterator and adds the ability to move backwards, check position, and modify or insert items at the current cursor location. You obtain one by calling list.listIterator() or list.listIterator(startIndex) to begin at a specific position.
The cursor model of ListIterator is worth thinking through carefully because it is a little different from what you might expect. The cursor sits between elements, not on them. After next() is called, the element to the left of the cursor is the one that was returned, and its index is previousIndex(). After previous() is called, the element to the right of the cursor is the one that was returned, and its index is nextIndex(). The set() and remove() methods always operate on the element most recently returned by either next() or previous().
ListIterator methods beyond standard Iterator
All five of these are absent from the basic Iterator interface and are only available for List types.
- hasPrevious():Returns true if there is an element before the current cursor position.
- previous():Returns the element before the cursor and moves the cursor back one step. Throws NoSuchElementException if hasPrevious() is false.
- nextIndex():Returns the index of the element that would be returned by the next call to next(). Returns list size if the iterator is at the end.
- previousIndex():Returns the index of the element that would be returned by the next call to previous(). Returns -1 if the iterator is at the beginning.
- set(E e):Replaces the element most recently returned by next() or previous() with the given element.
- add(E e):Inserts an element immediately before the element that next() would return, and immediately after the element that previous() would return.
ListIterator in Action
JavaForward and backward traversal, in-place replacement with set(), and insertion with add().
Creating a Custom Iterable Class
Implementing Iterable in your own class is one of the cleaner ways to make a domain type feel like a natural part of the Java language. If you build a NumberRange, a FileTree, or a Paginator class, implementing Iterable lets callers process your structure with a simple for-each loop, pass it to any method that accepts an Iterable, and even use it as a stream source via StreamSupport.stream(spliterator, false). None of that requires exposing the internal data structure.
The minimum implementation is two things: the outer class implements Iterable<T> and provides an iterator() method, and the Iterator it returns implements hasNext() and next(). The iterator is typically written as a private inner class or as an anonymous class inline. Private inner classes have access to the outer class's fields, which is exactly what the iterator needs to track position within the outer object's data.
Each call to iterator() must return a fresh, independent iterator object. Two separate for-each loops over the same collection must be able to run their own cursors without interfering with each other. This is not enforced by the compiler, but violating it produces very confusing bugs where one loop's position leaks into another.
Custom Iterable: NumberRange
JavaA class representing an integer range [start, end] that supports for-each, streams, and both step directions.
Custom Iterable with Remove Support
Adding remove() support to a custom iterator requires more design work because the iterator must communicate back to the outer class to update its data structure. The typical approach is to track the index of the last returned element and delegate the actual removal to the outer structure through a package-private or protected hook. The example below shows the pattern using a simple array-backed list.
Custom Iterable with remove() Support
JavaAn array-backed SimpleList that supports safe element removal through its iterator.
for-each with Arrays and Collections
Arrays and collections both work with for-each, but for a different reason in each case. Collections work because they implement Iterable. Arrays work through a separate compiler rule: when the compiler sees a for-each over an array type, it generates a traditional index-based loop rather than requesting an iterator. Arrays do not implement Iterable and cannot be passed to a method that accepts one, even though they look the same in a for-each statement.
This distinction has practical consequences. You cannot call iterator() on an array, you cannot pass an array to a utility method that accepts Iterable<T>, and you cannot use an array with forEach(Consumer) unless you first wrap it with Arrays.asList() or stream it with Arrays.stream(). For primitive arrays like int[], Arrays.asList() does not apply directly because it requires an object array. Arrays.stream(int[]) gives you an IntStream instead.
for-each with Arrays and Collections
JavaThe compiler differences, wrapping arrays as lists, streaming primitive arrays, and nested iteration.
Spliterator (Java 8+)
Java 8 introduced Spliterator as the underlying machinery that powers the Streams API. While you will rarely use Spliterator directly in day-to-day code, understanding it demystifies how parallel streams work and how custom data structures can integrate cleanly with the Streams API.
The name combines "split" and "iterator." A Spliterator does everything a regular iterator does, and it adds the ability to divide its remaining elements into two halves. The parallel stream machinery calls trySplit() repeatedly to partition the source across multiple threads. When the source cannot be split further (either because it is too small or because the underlying structure does not support it), trySplit() returns null.
Beyond splitting, a Spliterator carries characteristics(), a bitmask that describes properties of the source: whether it is ordered, distinct, sorted, sized (has a known count), non-null (guarantees no null elements), and immutable or concurrent. The stream framework uses these flags to skip unnecessary work. If a Spliterator reports DISTINCT, the stream knows it does not need to run a deduplication step. If it reports SIZED, the stream can pre-allocate the result container with the exact capacity needed.
Spliterator characteristics flags
These are bit constants on the Spliterator interface. The characteristics() method returns a bitmask combining whichever apply.
- ORDERED:The source has a defined encounter order. Lists are ORDERED; HashSets are not.
- DISTINCT:No two elements are equal (as determined by equals()). Sets are DISTINCT.
- SORTED:Elements are in a sorted order according to a Comparator or natural ordering. TreeSets are SORTED.
- SIZED:The estimateSize() method returns a precise count of remaining elements. ArrayLists are SIZED.
- NONNULL:Guarantees that no element returned by tryAdvance() will be null.
- IMMUTABLE:The source cannot be structurally modified during traversal.
- CONCURRENT:The source can be safely modified concurrently without external synchronization.
- SUBSIZED:All sub-spliterators produced by trySplit() will also be SIZED and SUBSIZED.
Spliterator Basics
JavaObtaining spliterators, reading characteristics, tryAdvance(), and manually splitting a source.
Plugging a Custom Iterable into Streams
Any class that implements Iterable automatically inherits a default spliterator() method that wraps its iterator in a basic Spliterator with unknown size and no special characteristics. You can then pass that spliterator to StreamSupport.stream() to get a full stream over your custom type. Overriding spliterator() with a more accurate implementation that reports the correct size and characteristics gives the stream framework better information to work with, which can lead to genuine performance improvements for large data sources.
Custom Iterable as a Stream Source
JavaConverting the earlier NumberRange into a stream via StreamSupport, then applying standard stream operations.
Quiz - Test Your Knowledge
Ten questions covering the Iterable and Iterator contracts, the for-each expansion, safe removal with iterator.remove(), ListIterator bidirectional traversal, building custom iterable classes, the difference between arrays and collections in for-each, and the Spliterator interface. Read each option carefully before selecting your answer.
Knowledge Check
1. What must a class implement to be usable in a Java for-each loop?
2. In what order must you call Iterator methods to safely step through a collection?
3. What happens if you call Iterator.remove() without first calling next()?
4. What distinguishes ListIterator from a standard Iterator?
5. What is the minimum you must implement to create a custom Iterable class in Java?
6. What is a ConcurrentModificationException and when does it occur?
7. Which statement about for-each loops and arrays is correct?
8. What is the primary purpose of a Spliterator compared to a regular Iterator?
9. What does Spliterator.trySplit() return when the source cannot be split further?
10. You have a custom NumberRange class representing a range [start, end]. Which approach correctly makes it usable in a for-each loop?