Definition
- tryAdvance(Consumer) – processes one element and returns true if more elements remain; combines hasNext() and next() into a single method
- trySplit() – splits the remaining elements: returns a new Spliterator for one portion while the current Spliterator retains the rest; returns null if splitting is not possible
- estimateSize() – returns the estimated count of remaining elements (exact if SIZED characteristic is present)
- characteristics() – returns a bitmask of properties (ORDERED, SIZED, SORTED, DISTINCT, NONNULL, IMMUTABLE, CONCURRENT, SUBSIZED)
- forEachRemaining(Consumer) – processes all remaining elements; default implementation calls tryAdvance repeatedly
- Internal use – parallel streams call trySplit() recursively to distribute chunks to ForkJoinPool worker threads
At National Bank of India, an Iterator is one clerk processing a stack of 1000 transaction receipts one by one. A Spliterator is a splitting clerk: when the manager asks, the clerk divides the stack in half and hands 500 receipts to a colleague. Each clerk processes their half simultaneously. When both finish, the results are combined. This is exactly how parallel streams work internally: the Spliterator decides how to split the data for parallel processing.
Understanding the Concepts
What is a Spliterator?
Spliterator (Splittable Iterator) was introduced in Java 8 alongside the Stream API. Unlike a regular Iterator that processes elements sequentially, a Spliterator can split its elements into two halves, enabling parallel processing. Every Collection provides a default spliterator() method.
package com.bank.demo;
import java.util.List;
import java.util.Spliterator;
public class SpliteratorBasicDemo {
public static void main(String[] args) {
List<String> accounts = List.of("SAV2001", "CUR3001", "FD4001", "SAV2002", "CUR3002");
Spliterator<String> spliterator = accounts.spliterator();
System.out.println("Estimated size: " + spliterator.estimateSize());
System.out.println("Characteristics: " + spliterator.characteristics());
// tryAdvance: process one element at a time
System.out.println("\nProcessing one by one:");
spliterator.tryAdvance(acc -> System.out.println(" Processed: " + acc));
spliterator.tryAdvance(acc -> System.out.println(" Processed: " + acc));
System.out.println("Remaining estimate: " + spliterator.estimateSize());
}
}
Estimated size: 5
Characteristics: 16721
Processing one by one:
Processed: SAV2001
Processed: CUR3001
Remaining estimate: 3
How does trySplit work?
trySplit divides the remaining elements approximately in half. The current Spliterator retains one portion, and a new Spliterator is returned for the other portion. This enables the ForkJoinPool to process both halves in parallel.
package com.bank.demo;
import java.util.ArrayList;
import java.util.List;
import java.util.Spliterator;
public class TrySplitDemo {
public static void main(String[] args) {
List<String> accounts = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
accounts.add("ACC-" + String.format("%03d", i));
}
Spliterator<String> original = accounts.spliterator();
System.out.println("Original size: " + original.estimateSize());
Spliterator<String> firstHalf = original.trySplit();
System.out.println("After split:");
System.out.println(" First half size: " + firstHalf.estimateSize());
System.out.println(" Second half size: " + original.estimateSize());
System.out.println("\nFirst half elements:");
firstHalf.forEachRemaining(acc -> System.out.println(" " + acc));
System.out.println("Second half elements:");
original.forEachRemaining(acc -> System.out.println(" " + acc));
}
}
Original size: 10
After split:
First half size: 5
Second half size: 5
First half elements:
ACC-001
ACC-002
ACC-003
ACC-004
ACC-005
Second half elements:
ACC-006
ACC-007
ACC-008
ACC-009
ACC-010
How do parallel streams use Spliterator internally?
When you call parallelStream(), the Stream API obtains a Spliterator from the collection and uses the ForkJoinPool to recursively split and process the data.
package com.bank.demo;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
public class ParallelStreamSpliteratorDemo {
record Account(String id, BigDecimal balance) {}
public static void main(String[] args) {
List<Account> accounts = new ArrayList<>();
for (int i = 1; i <= 1000; i++) {
accounts.add(new Account(
"ACC-" + String.format("%04d", i),
new BigDecimal(i * 1000)
));
}
// Sequential: uses Spliterator.forEachRemaining internally
BigDecimal seqTotal = accounts.stream()
.map(Account::balance)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// Parallel: uses Spliterator.trySplit to divide work
BigDecimal parTotal = accounts.parallelStream()
.map(Account::balance)
.reduce(BigDecimal.ZERO, BigDecimal::add);
System.out.println("Sequential total: " + seqTotal);
System.out.println("Parallel total: " + parTotal);
System.out.println("Equal: " + seqTotal.equals(parTotal));
}
}
Sequential total: 500500000
Parallel total: 500500000
Equal: true
How is Spliterator different from Iterator?
| Feature | Iterator | Spliterator |
|---|---|---|
| Split for parallel | No | Yes (trySplit) |
| Traverse one element | next() + hasNext() | tryAdvance(Consumer) |
| Bulk traverse | No built-in | forEachRemaining(Consumer) |
| Size estimate | No | estimateSize() |
| Characteristics | No | characteristics() flags |
| Introduced | Java 2 | Java 8 |
| Used by | for-each loops, Iterator pattern | Stream API, parallel streams |
package com.bank.demo;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
public class IteratorVsSpliteratorDemo {
public static void main(String[] args) {
List<String> accounts = List.of("SAV2001", "CUR3001", "FD4001");
// Iterator: sequential only
System.out.println("Iterator:");
Iterator<String> it = accounts.iterator();
while (it.hasNext()) {
System.out.println(" " + it.next());
}
// Spliterator: can split for parallel
System.out.println("\nSpliterator:");
Spliterator<String> sp = accounts.spliterator();
sp.forEachRemaining(acc -> System.out.println(" " + acc));
}
}
Iterator:
SAV2001
CUR3001
FD4001
Spliterator:
SAV2001
CUR3001
FD4001
What are common mistakes?
Mistake: Expecting trySplit to always split evenly
// trySplit may return null if the source is too small or cannot be split
Spliterator<String> sp = List.of("SAV2001").spliterator();
Spliterator<String> split = sp.trySplit();
System.out.println("Split result: " + split); // null: too few elements to split
Mistake: Using Spliterator directly instead of Stream API
// WRONG approach: manually managing Spliterators is verbose and error-prone
Spliterator<String> sp = accounts.spliterator();
// ... manual splitting, threading, merging
// CORRECT: let the Stream API handle Spliterator internally
accounts.parallelStream()
.filter(acc -> acc.startsWith("SAV"))
.forEach(System.out::println);
When should you create a custom Spliterator?
Create a custom Spliterator when you have a non-standard data source (a database cursor, a network stream, a custom tree structure) that you want to expose to the Stream API. Implement Spliterator with appropriate characteristics.
What should a senior engineer know?
- ArrayList vs LinkedList splitting. ArrayList's Spliterator splits by index range (O(1)), making it highly efficient for parallel streams. LinkedList's Spliterator traverses to the midpoint (O(n/2)), making it poor for parallel splitting.
- HashMap Spliterator. HashMap splits by bucket ranges, which can be uneven if the data distribution is skewed.
- trySplit contract. After trySplit, the original Spliterator covers a strict suffix and the returned one covers a strict prefix. estimateSize of both should sum to the original.
- StreamSupport.stream(). To create a Stream from a custom Spliterator, use
StreamSupport.stream(spliterator, parallel).
What are common interview follow-up questions?
- "What is the difference between Iterator and Spliterator?" : Spliterator can split for parallel processing via trySplit(), has estimateSize(), and reports characteristics().
- "When is trySplit called?" : By the ForkJoinPool when processing a parallel stream. It recursively splits until chunks are small enough.
- "Can trySplit return null?" : Yes. If the source is too small or indivisible, trySplit returns null to indicate no splitting is possible.
- "Do you interact with Spliterator directly?" : Rarely. The Stream API manages Spliterators internally. You create custom Spliterators only for non-standard data sources.
Visual Overview
Spliterator Parallel Processing Flow
=====================================
accounts.parallelStream().filter(...).collect(...)
Step 1: Get Spliterator
┌─────────────────────────────────────────┐
│ Spliterator [ACC-001 ... ACC-1000] │
│ estimateSize() = 1000 │
└─────────────────────────────────────────┘
Step 2: ForkJoinPool calls trySplit() recursively
┌─────────────────────┐ ┌──────────────────────┐
│ [ACC-001...ACC-500] │ │ [ACC-501...ACC-1000] │
└──────────┬──────────┘ └──────────┬───────────┘
│ │
trySplit() trySplit()
┌─────┴─────┐ ┌──────┴──────┐
[1..250] [251..500] [501..750] [751..1000]
Step 3: Each chunk processed by a ForkJoinPool thread
Thread-1: filter [1..250]
Thread-2: filter [251..500]
Thread-3: filter [501..750]
Thread-4: filter [751..1000]
Step 4: Results merged
┌────────────────────────────┐
│ Combined filtered results │
└────────────────────────────┘
Key Takeaway
Spliterator is the parallel-aware iterator that powers Java's Stream API. tryAdvance processes one element, trySplit divides the remaining elements for parallel processing, estimateSize reports the count, and characteristics tells the stream pipeline how to optimise. When you call parallelStream(), the ForkJoinPool recursively splits the Spliterator into smaller chunks, processes each chunk on a separate thread, and merges the results.
You rarely interact with Spliterators directly. The Stream API handles all the splitting and merging internally. The main reason to understand Spliterators is to know why certain collections parallelise better than others: ArrayList splits by index range (fast), while LinkedList traverses to the midpoint (slow). This knowledge helps you choose the right collection for parallel workloads.
At National Bank of India, when Pankaj needed to compute the total balance across 100,000 accounts in the end-of-day batch, he used parallelStream(). Internally, the ArrayList's Spliterator split the account list across ForkJoinPool threads, reducing the computation time proportionally to the number of cores.
Remember one sentence: Spliterator is a splittable iterator used internally by parallel streams, where trySplit divides data for concurrent processing and characteristics guide stream optimisations.
---