What is a Spliterator and what role does it play as the source abstraction behind a stream pipeline?
answer
- Splittable iterator: traverse + split
- tryAdvance / forEachRemaining / trySplit / estimateSize / characteristics
- trySplit() powers parallel streams (fork/join)
- Characteristics (SIZED/ORDERED/SORTED/DISTINCT…) drive optimizations
- StreamSupport.stream(spliterator, parallel) = custom source seam
basics
~20 sA Spliterator is the object that feeds elements from a source into a stream, one at a time. It is like an iterator that can also split itself in two, which is how streams divide work for parallel processing.
solid answer
~40 sA Spliterator (splittable iterator) is the low-level source abstraction every stream is built on. Where a classic Iterator only does hasNext/next, a Spliterator combines traversal and splitting: tryAdvance(action) consumes one element, forEachRemaining bulk-consumes, and trySplit() partitions the remaining elements into a second Spliterator so two threads can process halves independently — this is the mechanism behind parallel streams. It also exposes a characteristics bitmask (SIZED, ORDERED, SORTED, DISTINCT, IMMUTABLE, NONNULL, CONCURRENT…) and an estimateSize, which the stream framework uses to optimize: e.g. a SIZED+SUBSIZED ArrayList source splits evenly and presizes results, while a DISTINCT source lets distinct() be skipped. When you call collection.stream(), the collection supplies a Spliterator; StreamSupport.stream(spliterator, parallel) is the seam for building a stream over a custom source.
code
java · 30 lines// A minimal custom Spliterator producing 1..n, splittable for parallelism.
static final class RangeSpliterator implements Spliterator<Integer> {
private int cur;
private final int end; // exclusive
RangeSpliterator(int start, int end) { this.cur = start; this.end = end; }
@Override public boolean tryAdvance(Consumer<? super Integer> action) {
if (cur < end) { action.accept(cur++); return true; }
return false; // no elements left
}
@Override public Spliterator<Integer> trySplit() {
int remaining = end - cur;
if (remaining < 2) return null; // too small to split
int mid = cur + remaining / 2;
var prefix = new RangeSpliterator(cur, mid); // first half to a new spliterator
cur = mid; // this keeps the second half
return prefix;
}
@Override public long estimateSize() { return end - cur; }
@Override public int characteristics() {
return SIZED | SUBSIZED | ORDERED | IMMUTABLE | NONNULL | DISTINCT;
}
}
// Build a stream over the custom source:
// Stream<Integer> s = StreamSupport.stream(new RangeSpliterator(1, 11), /*parallel*/ true);go deeper
Knows that something supplies a stream's elements and can call it an iterator-like source; deep detail not expected.
Describes a Spliterator as a splittable iterator that feeds the stream and enables parallelism, naming tryAdvance and trySplit.
Explains characteristics (SIZED/ORDERED/SORTED/DISTINCT) and how trySplit + estimateSize drive fork/join parallel performance, and uses StreamSupport for custom sources.
Reasons about split balance and characteristics when designing custom sources or diagnosing poor parallel scaling, and weighs spliterator quality against the cost of parallelism.
## Where the elements actually come from A stream itself stores nothing — so *something* must supply its elements when the terminal operation runs. That something is a **`Spliterator`** (`java.util.Spliterator`, Java 8). The name = **splittable iterator**: it both *traverses* elements and can *split* the remaining ones into pieces for parallel work. ## Spliterator vs Iterator A classic `Iterator` has two methods: `hasNext()` and `next()`. A `Spliterator` is richer: - **`boolean tryAdvance(Consumer<T> action)`** — if an element remains, perform `action` on it and return `true`; otherwise return `false`. This is the single-element pull (replacing hasNext+next in one call, avoiding the two-call race). - **`void forEachRemaining(Consumer<T> action)`** — bulk-traverse all remaining elements; lets a source provide an optimized batch traversal. - **`Spliterator<T> trySplit()`** — try to **partition** the not-yet-traversed elements: it returns a *new* spliterator covering roughly the first half and leaves `this` covering the rest, or returns `null` if it cannot/should not split. This is the heart of parallelism. - **`long estimateSize()`** — an estimate (possibly `Long.MAX_VALUE` for unknown/infinite) of remaining elements; the fork/join machinery uses it to decide whether further splitting is worthwhile. - **`int characteristics()`** — a bitmask of properties (below). ## Characteristics — metadata that drives optimization The characteristics flags let the stream framework specialize behavior: - **SIZED** — `estimateSize` is exact → results can be presized, ranges split evenly. - **SUBSIZED** — children of a split are also SIZED → enables clean recursive splitting. - **ORDERED** — there is a defined encounter order (a List has it; a HashSet does not). - **SORTED** — elements follow a sort order (lets a redundant `sorted()` be elided). - **DISTINCT** — no duplicates (lets `distinct()` be skipped, e.g. a Set source). - **IMMUTABLE / CONCURRENT** — the source won't change, or can be safely traversed while concurrently modified (governs late-binding/fail-fast behavior). - **NONNULL** — no null elements. A stream over an `ArrayList` gets `SIZED | SUBSIZED | ORDERED`; a stream over a `TreeSet` adds `SORTED | DISTINCT`. These let the pipeline drop work and split efficiently. ## How it splits — and why that means parallelism Parallel streams use the **fork/join** framework. The terminal op recursively calls `trySplit()` to break the source into chunks, hands each chunk to a worker thread (each chunk processed via its own spliterator), and merges the partial results. A source that splits **evenly and cheaply** (array-backed, SIZED+SUBSIZED) parallelizes well; one that splits poorly (a `LinkedList`, or an `Iterator`-derived spliterator that can only split by buffering) gives little or no speedup. So the spliterator's quality directly determines parallel performance. ## Where it appears in normal code You rarely touch a Spliterator directly, but it underlies everything: ```java // collection.stream() internally does roughly: Spliterator<T> sp = collection.spliterator(); Stream<T> stream = StreamSupport.stream(sp, /* parallel */ false); ``` `StreamSupport.stream(spliterator, parallel)` is the public seam to build a stream over a **custom source** — you implement (or adapt) a Spliterator describing how to traverse and split your data. `Spliterators.spliteratorUnknownSize(iterator, characteristics)` adapts a plain Iterator when you have nothing better (yielding a poor splitter). ## Single-use ties back here Because the spliterator is *consumed* as the stream is traversed, and a stream is bound to one spliterator traversal, this is the concrete reason a stream is single-use: the spliterator has been advanced to exhaustion and cannot be rewound. ## Terms defined - **Bitmask / flags:** an integer whose individual bits each represent a boolean property; `characteristics()` ORs them together. - **Fork/join:** Java's divide-and-conquer parallelism framework that recursively splits work and combines results. - **Encounter order:** the order in which a source presents its elements (meaningful for List, not for HashSet). - **Late-binding:** the spliterator binds to the source's elements at first traversal, not at creation — relevant to whether mid-stream source changes are seen.
- Why does a parallel stream over an ArrayList typically outperform one over a LinkedList?ArrayList's spliterator is array-backed and SIZED+SUBSIZED, so trySplit() can index straight to the midpoint and divide elements evenly in O(1), with exact sizes for balanced fork/join work. LinkedList has no random access, so its spliterator splits poorly (often by buffering or uneven chunks), producing unbalanced tasks and little or negative speedup.
- How do the SORTED and DISTINCT characteristics let the pipeline skip work?If the source spliterator reports SORTED with a matching comparator, a redundant sorted() in the pipeline can be elided. If it reports DISTINCT (e.g. a Set source), a distinct() operation can be skipped because duplicates are already impossible. The framework reads characteristics() to make these optimizations transparently.
saying these in an interview costs you the question
- Calling a Spliterator just an Iterator with no extra capability — it splits and exposes characteristics
- Claiming all sources parallelize equally well (split quality varies; LinkedList splits poorly)
- Thinking estimateSize must be exact (it is an estimate; can be Long.MAX_VALUE)
- Confusing trySplit returning the second half — it returns a prefix and keeps the suffix in this