skip to content

How would you write a custom Spliterator, and when is doing so actually worth it?

level: principalimportance: nice to knowfreq 28%

answer

  1. Four methods: tryAdvance, trySplit, estimateSize, characteristics
  2. Three effort levels: wrap Iterator / AbstractSpliterator / full impl
  3. trySplit returns prefix, keeps suffix, null when too small; midpoint via >>> 1
  4. Honest flags only; SIZED+SUBSIZED need exact sizes
  5. Worth it for non-Collection sources or better splits; else use built-in

basics

~20 s

You implement tryAdvance (process one element), trySplit (hand off about half for parallelism), estimateSize (how many are left), and characteristics (the flags). You only bother when you have a custom data source that the built-in Collection/Stream tools can't traverse or parallelize well.

solid answer

~50 s

To write a custom Spliterator you implement four methods: tryAdvance (consume one element, return false at end), trySplit (return a new Spliterator over a prefix or null if not worth splitting), estimateSize (exact or Long.MAX_VALUE if unknown), and characteristics (an honest bitmask). For simple cases you extend Spliterators.AbstractSpliterator (which gives you a default binary-chopping trySplit) or wrap an Iterator via Spliterators.spliterator(...). You then expose a Stream with StreamSupport.stream(spliterator, parallel). It is worth writing your own only when: the source isn't a standard Collection (a paged API, a file/line source, a tree, a generator) and you need stream-based traversal, OR the default Spliterator splits poorly and you can do better (e.g. you know exact sizes or a cheap midpoint), OR you want correct characteristics for optimization. For ordinary collections, the built-in spliterator() is already excellent — don't reinvent it. A good custom Spliterator splits cheaply and balanced, and never reports a characteristic it doesn't honor.

go deeper

for a junior

Can say you implement tryAdvance and trySplit and that it is an advanced, rarely-needed task.

for a middle

Names the four methods and knows StreamSupport.stream wraps a Spliterator into a Stream; knows to prefer the built-in for normal collections.

for a senior

Implements a balanced, SIZED/SUBSIZED array-range Spliterator, chooses among wrap-Iterator / AbstractSpliterator / full-impl, and avoids dishonest flags.

for a principal

Decides when a custom Spliterator is justified by workload, designs cheap balanced splits with correct minimal characteristics, handles late-binding/fail-fast and primitive specializations, and weighs it against just using the built-in.

## The interface you must satisfy `Spliterator<T>` has four core methods. To implement one from scratch you provide: ```java boolean tryAdvance(Consumer<? super T> action); // consume one element Spliterator<T> trySplit(); // hand off ~half, or null long estimateSize(); // remaining count (or MAX_VALUE) int characteristics(); // honest flag bitmask ``` `forEachRemaining` and `getComparator` have defaults you can override for performance/SORTED support. ## The three levels of effort **1. Wrap an existing Iterator (least effort).** If you already have an `Iterator` and just want a Stream, don't write a Spliterator at all — adapt: ```java Spliterator<T> sp = Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED); Stream<T> stream = StreamSupport.stream(sp, false); ``` This gives you sequential streaming but **poor parallelism** (unknown size, weak splitting). Fine when the source is sequential anyway. **2. Extend `Spliterators.AbstractSpliterator` (moderate effort).** You implement only `tryAdvance` (and `estimateSize`); the abstract base supplies a generic `trySplit` that **batches** elements into arrays of geometrically growing size and returns array-backed Spliterators. This gives *some* parallelism without you writing split logic — a good middle ground for generator-like sources. **3. Implement the full interface (most control).** For a source with **random access or a cheap midpoint** (an array, a range, a balanced tree), write `trySplit` yourself to produce **balanced, O(1)** splits and report exact `SIZED | SUBSIZED`. This yields the best parallel performance. ## A complete example: splitting an array range ```java class ArrayRangeSpliterator<T> implements Spliterator<T> { private final T[] array; private int origin; // current position (inclusive) private final int fence; // end (exclusive) ArrayRangeSpliterator(T[] a, int origin, int fence) { this.array = a; this.origin = origin; this.fence = fence; } @Override public boolean tryAdvance(Consumer<? super T> action) { if (origin < fence) { action.accept(array[origin++]); return true; } return false; // exhausted } @Override public Spliterator<T> trySplit() { int lo = origin, mid = (lo + fence) >>> 1; if (lo >= mid) return null; // too small to split origin = mid; // this keeps [mid, fence) return new ArrayRangeSpliterator<>(array, lo, mid); // prefix [lo, mid) } @Override public long estimateSize() { return fence - origin; } @Override public int characteristics() { return ORDERED | SIZED | SUBSIZED; // honest flags for an array slice } } ``` Key design points visible here: - `tryAdvance` advances by exactly one and returns `false` when `origin == fence`. - `trySplit` computes a **balanced midpoint** with `(lo + fence) >>> 1` (the unsigned shift avoids overflow), returns the **prefix** as a new Spliterator, and keeps the suffix in `this`. It returns `null` when the range is too small. - `estimateSize` is **exact**, so `SIZED` is honest; because every split is also exact, `SUBSIZED` is honest too. - It reports `ORDERED` because array order is meaningful. Turn it into a stream: ```java Stream<T> s = StreamSupport.stream(new ArrayRangeSpliterator<>(arr, 0, arr.length), true); // parallel ``` ## When it is actually worth it Writing a custom Spliterator is **rarely necessary**. Justify it when: 1. **The source isn't a standard Collection** and you want stream traversal — e.g. a **paged remote API**, a **file read line-by-line**, a **custom tree/graph**, or a **generator**. (Often level 1 or 2 suffices.) 2. **The default splits poorly but you can do better** — you know an exact size or a cheap O(1) midpoint that the generic `AbstractSpliterator` batching can't match, and the workload is parallel-heavy enough to care. 3. **You need correct characteristics** the framework can optimize on (exact `SIZED`, `DISTINCT`, `SORTED` with a comparator) that a generic adapter wouldn't report. If the data is already in an `ArrayList`/array/`TreeSet`, the **built-in `spliterator()` is already excellent** — reinventing it is wasted effort and a correctness risk. ## Correctness pitfalls to avoid - **Never report a characteristic you don't honor** — false `DISTINCT`/`SIZED`/`SORTED` corrupts stream results. - **`trySplit` must eventually return `null`** and must partition without overlap or loss. - **`tryAdvance` must call the consumer exactly once per element** and return `false` only at end. - **Late vs early binding** — decide when the Spliterator commits to the source's contents (most JDK ones are *late-binding*: they bind at first traversal/split, and are fail-fast on subsequent structural modification). Document and implement this consistently. - For primitives, implement **`Spliterator.OfInt/OfLong/OfDouble`** to avoid boxing.

  • What does Spliterators.AbstractSpliterator give you for free, and what is its limitation?
    It supplies a generic trySplit that batches elements into geometrically growing arrays so a sequential tryAdvance-only source gains some parallelism. Its limitation is that splits aren't as balanced or cheap as a true random-access split and sizes are usually unknown, so it can't match a hand-written O(1) midpoint split with exact SIZED/SUBSIZED.
  • What is late-binding for a Spliterator?
    A late-binding Spliterator does not capture the source's elements at construction time; it binds at first traversal or first split. Most JDK Spliterators are late-binding and fail-fast: structural modifications to the source before binding are seen, but modifications after binding may trigger ConcurrentModificationException.

saying these in an interview costs you the question

  • Writing a custom Spliterator for data already in an ArrayList — the built-in is better
  • Reporting SIZED/DISTINCT/SORTED without honoring them
  • trySplit that peels one element each call (unbalanced, no parallel benefit) or never returns null
  • Forgetting OfInt/OfLong/OfDouble and boxing every primitive
  • Assuming a wrapped Iterator (unknown size) parallelizes well — it usually doesn't

context