CodeGym /Courses /JAVA 25 SELF /Spliterator and parallel streams

Spliterator and parallel streams

JAVA 25 SELF
Level 33, Lesson 3
Available

1. Introduction to Spliterator

If you thought that collections in Java are iterated only via Iterator, you were absolutely right up until Java 8. But with the arrival of the Stream API and the trend toward parallelism, a new hero appeared — Spliterator.

Spliterator is an interface that allows not only iterating over a collection’s elements, but also splitting the data source into parts for parallel processing. The name is a blend of the words split and iterator.

Imagine a big cake. A regular Iterator cuts it piece by piece and eats in order. Spliterator can cut the cake in half, give one half to a friend — and you both start eating at the same time. Got more friends — split further!

The Spliterator interface — core methods

public interface Spliterator<T> {
    boolean tryAdvance(java.util.function.Consumer<? super T> action);
    Spliterator<T> trySplit();
    long estimateSize();
    int characteristics();
    // ... a couple more methods, but these are the most important
}
  • tryAdvance — does something with the next element (analogous to next() + an action).
  • trySplit — tries to split the source into two parts and return a new Spliterator for the ‘split-off’ part.
  • estimateSize — estimates how many elements remain.
  • characteristics — returns a bitmask of characteristics (ordering, uniqueness, immutability, etc.).

2. Using Spliterator: manual traversal and splitting

Obtaining a Spliterator from a collection

Any collection implementing Collection can provide its Spliterator:

import java.util.List;
import java.util.Spliterator;

List<String> names = List.of("Alice", "Bob", "Charlie", "Diana");
Spliterator<String> spliterator = names.spliterator();

Manual element traversal

Spliterator<String> spliterator = names.spliterator();
while (spliterator.tryAdvance(name -> System.out.println("Name: " + name))) {
    // Everything happens inside tryAdvance
}

Splitting a collection

The most interesting part is the trySplit() method:

Spliterator<String> spliterator1 = names.spliterator();
Spliterator<String> spliterator2 = spliterator1.trySplit();

System.out.println("First part:");
spliterator1.forEachRemaining(System.out::println);

System.out.println("Second part:");
if (spliterator2 != null) {
    spliterator2.forEachRemaining(System.out::println);
}

What will happen: the Spliterator will try to split the collection into two parts (not always exactly in half — it depends on the implementation). Now you can process both parts independently — even in different threads!

3. Parallel streams: why and how this works

A parallel stream (parallelStream()) is a stream that processes elements not sequentially, but concurrently across several threads. It is especially useful for large data volumes and multi-core processors.

import java.util.List;

List<String> names = List.of("Alice", "Bob", "Charlie", "Diana");
// Sequential stream:
names.stream().forEach(System.out::println);
// Parallel stream:
names.parallelStream().forEach(System.out::println);

What’s the point?
In a sequential stream, elements are processed in a single thread. In a parallel stream, the source is split into parts (using a Spliterator), and each part is processed in a separate thread.

How does it work under the hood?

  1. The Spliterator splits the collection into parts — usually by the number of available cores (or slightly more).
  2. Each part is processed in its own thread — the common ForkJoinPool is used.
  3. The results are gathered back — combined into the final collection or value.

Parallel stream workflow diagram

flowchart LR
    A[Collection] --> B{Spliterator}
    B --> C1[Part 1] --> D1[Thread 1]
    B --> C2[Part 2] --> D2[Thread 2]
    B --> C3[Part 3] --> D3[Thread 3]
    D1 & D2 & D3 --> E[Result assembly]

4. Advantages and limitations of parallel streams

Advantages

  • Speedup for large collections: with compute‑intensive operations, a parallel stream noticeably accelerates execution.
  • Simplicity: no need to write multithreaded code manually — replace stream() with parallelStream().

Limitations and pitfalls

  • Not always faster: for small collections, overhead can eat any benefit.
  • Order is not guaranteed: in forEach/map/filter the order may differ. If you need ordering, use forEachOrdered.
  • Thread-safety issues: operations with side effects (mutating external collections/variables) lead to data races.
  • Not all operations fit: dependent computations (e.g., sequential accumulation) may not behave as expected.

When to use parallel streams?

  • Large collections (tens of thousands of elements or more).
  • Heavy work per element.
  • Strict order is not critical.
  • No side effects (pure functions).

When NOT to use them?

  • Few elements.
  • The code mutates external variables or collections.
  • You must preserve processing order.
  • The data source splits poorly (for example, LinkedList).

5. Practical examples

Example 1: Comparing execution time

import java.util.*;
import java.util.stream.*;

public class ParallelStreamDemo {
    public static void main(String[] args) {
        List<Integer> numbers = IntStream.range(0, 10_000_000)
                                         .boxed()
                                         .collect(Collectors.toList());

        long start = System.currentTimeMillis();
        long count = numbers.stream()
                .filter(n -> isPrime(n))
                .count();
        long time = System.currentTimeMillis() - start;
        System.out.println("Sequential stream: " + time + " ms, primes found: " + count);

        start = System.currentTimeMillis();
        count = numbers.parallelStream()
                .filter(n -> isPrime(n))
                .count();
        time = System.currentTimeMillis() - start;
        System.out.println("Parallel stream: " + time + " ms, primes found: " + count);
    }

    // A very simple primality check (for example)
    public static boolean isPrime(int n) {
        if (n < 2) return false;
        for (int i = 2, sqrt = (int)Math.sqrt(n); i <= sqrt; i++)
            if (n % i == 0) return false;
        return true;
    }
}

What you’ll see: on large data volumes, a parallel stream is often faster (especially on multi-core processors). On small inputs, there may be no difference, or the parallel variant may even be slower.

Example 2: Ordering issue

import java.util.List;

List<String> names = List.of("Alice", "Bob", "Charlie", "Diana");
System.out.println("Sequential stream:");
names.stream().forEach(System.out::println);

System.out.println("Parallel stream:");
names.parallelStream().forEach(System.out::println);

System.out.println("Parallel stream with forEachOrdered:");
names.parallelStream().forEachOrdered(System.out::println);

Output: in a sequential stream and when using forEachOrdered, the order is preserved, whereas in a parallel stream without it — it is not.

Example 3: The danger of side effects

import java.util.*;
import java.util.stream.*;

List<Integer> numbers = IntStream.range(1, 1000).boxed().collect(Collectors.toList());
List<Integer> results = new ArrayList<>();

// DANGEROUS! Don't do this!
numbers.parallelStream().forEach(n -> results.add(n * n));

System.out.println("List size: " + results.size());

What can happen? The list size may be smaller than expected, and sometimes you’ll get a ConcurrentModificationException. The reason is that ArrayList is not thread-safe, while a parallel stream runs several threads simultaneously.

6. Spliterator: features and characteristics

Spliterator characteristics

The Spliterator describes its properties via a bitmask:

  • ORDERED — elements come in a defined order (for example, a list).
  • DISTINCT — all elements are unique (for example, a set).
  • SORTED — elements are sorted.
  • SIZED — the size is known.
  • IMMUTABLE — the collection is immutable.
  • CONCURRENT — the collection is thread-safe.
  • SUBSIZED — all spliterators after trySplit() also know their size.
Spliterator<String> spliterator = names.spliterator();
int characteristics = spliterator.characteristics();
System.out.println(Integer.toBinaryString(characteristics));

Why does this matter? The Stream API and parallel streams use these flags for optimizations. For example, if the source is immutable and sorted, it can be split and the result collected more safely and efficiently.

7. When and how to use Spliterator directly?

In everyday work you rarely need to write your own Spliterators: standard collections already implement everything. But if you’re creating your own data source or want fine-grained control over traversal/splitting, Spliterator will come in handy.

Example: manual traversal with tryAdvance

import java.util.List;
import java.util.Spliterator;

List<String> names = List.of("Alice", "Bob", "Charlie", "Diana");
Spliterator<String> spliterator = names.spliterator();
spliterator.tryAdvance(name -> System.out.println("First element: " + name));
spliterator.forEachRemaining(name -> System.out.println("Remaining: " + name));

Example: splitting a collection

Spliterator<String> spliterator1 = names.spliterator();
Spliterator<String> spliterator2 = spliterator1.trySplit();

if (spliterator2 != null) {
    spliterator2.forEachRemaining(name -> System.out.println("Part 2: " + name));
}
spliterator1.forEachRemaining(name -> System.out.println("Part 1: " + name));

8. Common mistakes when working with Spliterator and parallel streams

Error #1: Using parallel streams for small collections. Instead of a speedup, you’ll get a slowdown — the overhead of splitting and task scheduling outweighs the benefit.

Error #2: Expecting element order to be preserved. Parallel streams don’t guarantee order. If it matters, use forEachOrdered, but you’ll lose part of the parallel efficiency.

Error #3: Side effects in lambda expressions. Inside a parallel stream you can’t safely mutate external variables/collections — you’ll get data races and hard‑to‑catch bugs.

Error #4: Using non‑thread‑safe collections inside a parallel stream. Adding to a regular ArrayList from multiple threads is a straight path to errors like ConcurrentModificationException.

Error #5: Expecting instant speedup. Parallel streams aren’t a magic wand. Profile: if the data volume is small or the operation is light, a sequential stream is faster.

Error #6: Parallel streams with sources that split poorly. For example, LinkedList often splits inefficiently — parallelism can only slow execution down.

1
Task
JAVA 25 SELF, level 33, lesson 3
Locked
Processing the first received package at the warehouse 📦
Processing the first received package at the warehouse 📦
1
Task
JAVA 25 SELF, level 33, lesson 3
Locked
Dividing tasks between teams at a strategic meeting 🎯
Dividing tasks between teams at a strategic meeting 🎯
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION