Implementing Iterable and Iterator: Custom For-Each Support in Java

The enhanced for-each loop works with any class that implements java.lang.Iterable. Implementing Iterable makes your custom data structures (trees, graphs, ranges, file line sequences) participatein for-each loops, Stream.of(), Collections utility methods, and any framework that accepts an Iterable. This article covers the Iterable and Iterator contracts, implementing them on a custom collection, lazy/infinite iterators, the fail-fast modification-check pattern, and the Spliterator interface for stream support.

The Iterable and Iterator Contracts

The Java enhanced for-each loop, for (T item : collection), is syntactic sugar. The compiler rewrites it into an explicit iterator loop at compile time. For that rewriting to work, the expression after the colon must implement java.lang.Iterable<T>, which has exactly one method: Iterator<T> iterator(). Calling iterator() returns a fresh cursor object that traverses the collection.

Iterator<T> is a separate interface with two mandatory methods: boolean hasNext(), which returns true if more elements remain, and T next(), which advances the cursor and returns the current element. A third method, void remove(), has a default implementation that throws UnsupportedOperationException: override it only if your iterator supports safe element removal during traversal.

The contract is: next() must not be called after hasNext() returns false, doing so should throw NoSuchElementException. Each call to iterator() on the Iterable must produce an independent cursor that starts from the beginning, iterating the same collection twice concurrently must work correctly.

Implementing Iterable on a Custom Collection

As a concrete example, consider a Range class that represents an integer range from a start value (inclusive) to an end value (exclusive). Instead of materialising all integers into a list, it lazily generates them on demand: the iterator holds only a single current counter. This is memory-efficient for large ranges and demonstrates the power of the iterator pattern: the consumer (the for-each loop) drives the iteration, pulling values one at a time.

Java
import java.util.Iterator;
import java.util.NoSuchElementException;

public class Range implements Iterable<Integer> {
    private final int start;
    private final int end;   // exclusive

    public Range(int start, int end) {
        if (start > end) throw new IllegalArgumentException(
            "start must be <= end, got " + start + " and " + end);
        this.start = start;
        this.end   = end;
    }

    @Override
    public Iterator<Integer> iterator() {
        // Return a fresh iterator each time — independent cursors
        return new Iterator<>() {
            private int current = start;

            @Override
            public boolean hasNext() {
                return current < end;
            }

            @Override
            public Integer next() {
                if (!hasNext()) throw new NoSuchElementException();
                return current++;
            }
        };
    }
}

// Usage — works with for-each, streams, and collection utilities
Range r = new Range(1, 6);

for (int n : r) {
    System.out.print(n + " "); // 1 2 3 4 5
}
System.out.println();

// Works with StreamSupport
import java.util.stream.StreamSupport;
int sum = StreamSupport.stream(r.spliterator(), false)
                       .mapToInt(Integer::intValue)
                       .sum();
System.out.println("Sum: " + sum); // 15

// Two independent iterators on the same Range
Iterator<Integer> it1 = r.iterator();
Iterator<Integer> it2 = r.iterator();
System.out.println(it1.next()); // 1
System.out.println(it2.next()); // 1 — independent from it1

Implementing a Fail-Fast Iterator

Java's standard collections use fail-fast iterators: if the underlying collection is structurally modified (elements added or removed) through any means other than the iterator's own remove() method while iteration is in progress, the iterator throws ConcurrentModificationException immediately. This is a best-effort safety mechanism: it detects concurrent modification early so that you see a clear error rather than silently corrupting data or producing wrong results.

The standard implementation technique is a modification counter: the collection maintains an int modCount field that it increments on every structural change. Each iterator captures the current modCount at creation time into expectedModCount. At the start of every next() call the iterator checks that modCount == expectedModCount; a mismatch triggers the exception.

Java
import java.util.*;

public class SimpleList<E> implements Iterable<E> {
    private Object[] elements;
    private int size;
    private int modCount = 0;   // incremented on every structural change

    public SimpleList() { elements = new Object[10]; }

    public void add(E e) {
        if (size == elements.length) elements = Arrays.copyOf(elements, size * 2);
        elements[size++] = e;
        modCount++;             // structural modification
    }

    public boolean remove(int index) {
        Objects.checkIndex(index, size);
        System.arraycopy(elements, index + 1, elements, index, size - index - 1);
        elements[--size] = null;
        modCount++;             // structural modification
        return true;
    }

    @Override
    public Iterator<E> iterator() {
        final int expectedMod = modCount;   // snapshot at iterator creation time
        return new Iterator<>() {
            int cursor = 0;

            @Override
            public boolean hasNext() {
                return cursor < size;
            }

            @Override
            @SuppressWarnings("unchecked")
            public E next() {
                if (modCount != expectedMod)
                    throw new ConcurrentModificationException();
                if (!hasNext()) throw new NoSuchElementException();
                return (E) elements[cursor++];
            }
        };
    }
}

SimpleList<String> list = new SimpleList<>();
list.add("a"); list.add("b"); list.add("c");

Iterator<String> it = list.iterator();
it.next();          // "a"
list.add("d");      // structural modification after iterator created
it.next();          // throws ConcurrentModificationException
Fail-fast behaviour is guaranteed only on a best-effort basis. It relies on the modCount check, which can theoretically be fooled by concurrent access from multiple threads without synchronisation. For truly safe concurrent iteration, use CopyOnWriteArrayList or a thread-safe wrapper.

Lazy and Infinite Iterators

One of the most useful applications of custom iterators is lazy generation, producing values on demand rather than precomputing them all. This is particularly valuable when the sequence is infinite (all Fibonacci numbers, all prime numbers) or when generating all values upfront would be prohibitively expensive (lines in a file, records from a database cursor).

A lazy iterator holds just enough state to compute the next value when asked. The consumer controls how many values are consumed: it can stop at any point without wasting memory or computation on values it never requested.

Java
import java.math.BigInteger;

// Infinite Fibonacci sequence — only computes what the caller consumes
public class Fibonacci implements Iterable<BigInteger> {

    @Override
    public Iterator<BigInteger> iterator() {
        return new Iterator<>() {
            private BigInteger a = BigInteger.ZERO;
            private BigInteger b = BigInteger.ONE;

            @Override
            public boolean hasNext() {
                return true; // infinite sequence
            }

            @Override
            public BigInteger next() {
                BigInteger result = a;
                BigInteger next = a.add(b);
                a = b;
                b = next;
                return result;
            }
        };
    }
}

// Consume only the first 10 Fibonacci numbers
Iterator<BigInteger> fib = new Fibonacci().iterator();
for (int i = 0; i < 10; i++) {
    System.out.print(fib.next() + " ");
}
// 0 1 1 2 3 5 8 13 21 34

// With StreamSupport — take first N, then stop
long count = StreamSupport.stream(new Fibonacci().spliterator(), false)
    .limit(20)
    .filter(n -> n.mod(BigInteger.TWO).equals(BigInteger.ZERO))
    .count();
System.out.println("Even Fibonacci numbers in first 20: " + count); // 7

Spliterator: Enabling Parallel Streams

The Spliterator interface, introduced in Java 8, extends the traversal capabilities of Iterator to support parallel decomposition. While Iterator is sequential, a Spliterator can split itself into two parts so that the Stream framework can assign each half to a different CPU core. Iterable has a default spliterator() method that wraps the Iterator in a Spliterators.spliteratorUnknownSize(): it works but cannot be split and therefore parallelism is limited.

For collections that know their size and structure, implementing Spliterator directly enables efficient parallel streams. The key method is trySplit(): it returns a new Spliterator covering roughly half the remaining elements, leaving the original to cover the other half. If splitting is not possible (the sequence is too small or unsplittable), return null.

Java
import java.util.Spliterator;
import java.util.function.Consumer;

// Spliterator for the Range class — enables parallel stream support
public class RangeSpliterator implements Spliterator<Integer> {
    private int current;
    private final int end;

    public RangeSpliterator(int start, int end) {
        this.current = start;
        this.end     = end;
    }

    @Override
    public boolean tryAdvance(Consumer<? super Integer> action) {
        if (current < end) {
            action.accept(current++);
            return true;
        }
        return false;
    }

    @Override
    public Spliterator<Integer> trySplit() {
        int remaining = end - current;
        if (remaining < 2) return null; // too small to split
        int mid = current + remaining / 2;
        int splitStart = current;
        current = mid;                   // this spliterator takes [mid, end)
        return new RangeSpliterator(splitStart, mid); // new one takes [start, mid)
    }

    @Override public long estimateSize() { return end - current; }
    @Override public int characteristics() {
        return ORDERED | SIZED | SUBSIZED | IMMUTABLE | DISTINCT | SORTED;
    }
}

// Use it
Spliterator<Integer> sp = new RangeSpliterator(0, 1_000_000);
long sum = StreamSupport.stream(sp, true) // true = parallel
    .mapToLong(Integer::longValue)
    .sum();
System.out.println(sum); // 499999500000
The characteristics() flags you declare on a Spliterator help the Stream engine choose optimal algorithms. SIZED enables more accurate work splitting; SORTED enables sorted-merge optimisations; DISTINCT can skip deduplication. Declare only the flags you can actually guarantee.