ZetCode

Java Spliterator

Last modified: May 8, 2025

The java.util.Spliterator is a specialized iterator introduced in Java 8 to efficiently traverse and partition elements of a data source. It is designed with parallel processing in mind, making it an essential component of the Streams API. Unlike conventional iterators, a Spliterator can divide a data source into multiple segments, allowing independent processing of each portion and improving performance in concurrent environments.

One of the key features of Spliterator is its ability to describe the nature of the data source through the characteristics method. These characteristics include:

Understanding these characteristics allows for better optimization of parallel execution. The metadata provided by the Spliterator enables the Java runtime to make informed decisions about how data should be processed efficiently.

Spliterator Basics

A Spliterator serves a dual purpose: it facilitates iteration through elements and enables parallel decomposition of a data source. This is achieved through a set of core operations:

Spliterator instances are commonly obtained from collections using the spliterator method, but they are not limited to lists and sets. They can also be constructed from arrays, streams, I/O channels, or even custom generator functions. The Streams API internally leverages Spliterator to enable efficient parallel computation, making it a powerful mechanism for processing large datasets in modern applications.

Basic Spliterator Usage

This example demonstrates how to use a Spliterator to traverse elements in a List. Unlike traditional iteration methods, Spliterator provides controlled traversal with characteristics that define its behavior. Here, we create a Spliterator from a predefined list of names and process each element one at a time using the tryAdvance method.

Main.java
package com.zetcode;

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

public class Main {

    public static void main(String[] args) {

        List<String> names = List.of("John", "Jane", "Doe", "Sarah");

        Spliterator<String> spliterator = names.spliterator();

        System.out.println("Characteristics: " + spliterator.characteristics());
        System.out.println("Estimated size: " + spliterator.estimateSize());

        System.out.println("Elements:");

        while (true) {
            boolean advanced = spliterator.tryAdvance(name -> System.out.println("Processing: " + name));
            if (!advanced) {
                break; // Exit loop when all elements are processed
            }
        }
    }
}

The output displays key properties of the Spliterator. The characteristics value 16464 indicates that the spliterator maintains element order (ORDERED), has a known size (SIZED), and supports efficient partitioning (SUBSIZED). It also estimates that there are four elements in the sequence.

After listing these attributes, the program proceeds to process each element individually. The tryAdvance method ensures that elements are accessed sequentially, applying the provided action (printing the name) before advancing. Once all elements are processed, tryAdvance returns false, signaling the loop to terminate.

This approach is useful for structured data traversal where elements need to be processed independently. Additionally, Spliterator can be leveraged for parallel execution when split appropriately, optimizing performance in multithreaded environments.

Splitting a Spliterator

A Spliterator is designed for both sequential iteration and efficient data partitioning, making it an essential tool for optimizing performance. One of its most powerful features is the trySplit method, which attempts to divide the Spliterator into separate portions. This enables workloads to be processed concurrently, improving efficiency in applications handling large datasets.

However, it is important to note that simply calling trySplit does not automatically enable parallel execution. The split operation only divides the data source into multiple Spliterator instances, but these instances will still process data sequentially unless explicitly parallelized using parallel streams or multi-threading techniques.

In this example, we demonstrate how a Spliterator can be split into two portions using trySplit. The method attempts to partition the elements of the data source into two roughly equal parts, returning a new Spliterator that manages the second half.

Main.java
package com.zetcode;

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

public class Main {

    public static void main(String[] args) {

        List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8);

        Spliterator<Integer> firstSplit = numbers.spliterator();
        Spliterator<Integer> secondSplit = firstSplit.trySplit();

        System.out.println("First split:");
        firstSplit.forEachRemaining(System.out::println);

        System.out.println("Second split:");
        secondSplit.forEachRemaining(System.out::println);
    }
}

The output illustrates how the original sequence is divided between the two Spliterator instances. The exact split point depends on the implementation, but generally, the method aims for an even partition of the dataset. However, despite splitting the elements, this example still runs sequentially—both splits are processed one after the other.

Enabling Parallel Execution

To truly leverage parallel execution, developers need to integrate parallel streams or explicit multi-threading strategies. The easiest way to parallelize processing is using Java's Stream API:

List numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8);

System.out.println("Processing in parallel:");
numbers.parallelStream().forEach(System.out::println);

The parallelStream method automatically distributes elements across multiple threads using Java's Fork/Join framework, ensuring concurrent execution without manual threading management. This is a recommended approach for parallel processing when working with collections.

However, when working with Spliterator, parallel execution does not happen automatically. The trySplit method divides a data source into separate Spliterator instances, but those parts are still processed sequentially unless explicitly parallelized.

Using Spliterator for Explicit Multi-Threading

For more control over parallel execution, developers can manually create threads or use an ExecutorService to process each split independently. This approach ensures proper parallel execution of segmented data.

Main.java
package com.zetcode;

import java.util.List;
import java.util.Spliterator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {

    public static void main(String[] args) {

        List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8);

        Spliterator<Integer> firstSplit = numbers.spliterator();
        Spliterator<Integer> secondSplit = firstSplit.trySplit();

        // Using an ExecutorService to process splits in parallel
        try (ExecutorService executor = Executors.newFixedThreadPool(2)) {

            executor.submit(() -> {
                System.out.println("First split processing by " + Thread.currentThread().getName());
                firstSplit.forEachRemaining(num -> System.out.println(Thread.currentThread().getName() + " - " + num));
            });

            executor.submit(() -> {
                System.out.println("Second split processing by " + Thread.currentThread().getName());
                secondSplit.forEachRemaining(num -> System.out.println(Thread.currentThread().getName() + " - " + num));
            });

            executor.shutdown();
        }
    }
}

This implementation correctly enables parallel execution using Spliterator. By employing ExecutorService with a fixed thread pool, each split is processed independently in a separate thread, ensuring true concurrency.

Choosing the Right Approach

Parallel streams are the simplest way to execute operations concurrently, making them ideal for general-purpose collection processing. On the other hand, manual threading with Spliterator provides finer control, allowing developers to explicitly distribute workloads across multiple threads.

Understanding the distinction between splitting a Spliterator and actually executing operations in parallel is crucial for optimizing performance in Java applications. By combining trySplit with parallel processing strategies, developers can create scalable solutions that efficiently handle large data sources.

Characteristics of Spliterator

This example demonstrates how to examine the characteristics of a Spliterator. Characteristics provide metadata about the data source that can be used to optimize processing. We create Spliterators from different sources and examine their characteristics.

Main.java
package com.zetcode;

import java.util.HashSet;
import java.util.List;
import java.util.Spliterator;
import java.util.TreeSet;
import java.util.stream.Stream;

public class Main {

    public static void main(String[] args) {

        printCharacteristics("List", List.of(1, 2, 3).spliterator());
        printCharacteristics("Set", new HashSet<>(List.of(1, 2, 3)).spliterator());
        printCharacteristics("SortedSet", new TreeSet<>(List.of(1, 2, 3)).spliterator());
        printCharacteristics("Stream", Stream.of(1, 2, 3).spliterator());
    }

    private static void printCharacteristics(String source, Spliterator<Integer> spliterator) {

        System.out.println(source + " characteristics:");
        System.out.println("ORDERED: " + spliterator.hasCharacteristics(Spliterator.ORDERED));
        System.out.println("DISTINCT: " + spliterator.hasCharacteristics(Spliterator.DISTINCT));
        System.out.println("SORTED: " + spliterator.hasCharacteristics(Spliterator.SORTED));
        System.out.println("SIZED: " + spliterator.hasCharacteristics(Spliterator.SIZED));
        System.out.println("NONNULL: " + spliterator.hasCharacteristics(Spliterator.NONNULL));
        System.out.println("IMMUTABLE: " + spliterator.hasCharacteristics(Spliterator.IMMUTABLE));
        System.out.println("CONCURRENT: " + spliterator.hasCharacteristics(Spliterator.CONCURRENT));
        System.out.println("SUBSIZED: " + spliterator.hasCharacteristics(Spliterator.SUBSIZED));
        System.out.println();
    }
}

The output shows how different collection types have different characteristics. For example, a List is ORDERED and SIZED, a Set is DISTINCT, and a SortedSet is also SORTED. These characteristics help the Stream API optimize its operations.

Creating a Custom Spliterator

This example demonstrates how to create a custom Spliterator to efficiently generate a sequence of even numbers up to a specified limit. Unlike traditional iteration mechanisms that rely on lists or arrays, Spliterator enables structured traversal of non-collection data sources, making it highly versatile for handling custom datasets.

In this implementation, a specialized Spliterator produces even numbers starting from 0 and incrementing by 2 until reaching the defined maximum value. Additionally, Spliterator supports parallel execution by dividing its workload into independent sections.

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

public class Main {

    static class EvenNumberSpliterator implements Spliterator<Integer> {

        private int current = 0;
        private final int max;
        
        public EvenNumberSpliterator(int max) {
            this.max = max;
        }
        
        @Override
        public boolean tryAdvance(Consumer<? super Integer> action) {
            if (current <= max) {
                action.accept(current);
                current += 2;
                return true;
            }
            return false;
        }
        
        @Override
        public Spliterator<Integer> trySplit() {
            int mid = (max - current) / 2;
            if (mid <= 1) return null;
            
            int newCurrent = current + mid + (mid % 2);
            EvenNumberSpliterator newSplit = new EvenNumberSpliterator(newCurrent - 2);
            current = newCurrent;
            return newSplit;
        }
        
        @Override
        public long estimateSize() {
            return (max - current) / 2 + 1;
        }
        
        @Override
        public int characteristics() {
            return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.NONNULL;
        }
    }
    
    public static void main(String[] args) {
        Spliterator<Integer> spliterator = new EvenNumberSpliterator(20);
        
        System.out.println("Even numbers:");
        
        boolean hasMoreElements;
        do {
            hasMoreElements = spliterator.tryAdvance(System.out::println);
        } while (hasMoreElements);
        
        // Parallel processing demonstration
        Spliterator<Integer> parallelSplit = new EvenNumberSpliterator(20);
        Spliterator<Integer> split1 = parallelSplit.trySplit();
        
        System.out.println("First half:");
        split1.forEachRemaining(System.out::println);
        
        System.out.println("Second half:");
        parallelSplit.forEachRemaining(System.out::println);
    }
}

This custom Spliterator efficiently generates and processes even numbers while supporting controlled iteration. The tryAdvance method ensures sequential traversal of elements one at a time, and instead of an empty while loop, the revised implementation now uses a do-while loop, ensuring at least one element is processed before checking termination conditions.

A key feature of this Spliterator is its ability to facilitate parallel computation. The trySplit method divides the number range into smaller partitions, allowing independent processing. This functionality is particularly useful in concurrent applications, where workloads can be efficiently distributed across multiple threads.

Spliterator is a powerful alternative to traditional iteration techniques, offering greater flexibility for structured data traversal and parallel execution. Whether handling large datasets or optimizing performance, its ability to split work dynamically makes it a valuable tool in modern Java applications.

Using Spliterator with Streams

This example shows how Spliterator is used internally by the Stream API. We'll create a stream from a Spliterator and demonstrate both sequential and parallel processing. This shows the real power of Spliterator in enabling parallel stream operations.

Main.java
package com.zetcode;

import java.util.List;
import java.util.Spliterator;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public class Main {

    public static void main(String[] args) {

        List<String> words = List.of("apple", "banana", "cherry", 
            "date", "elderberry");

        // Create stream from Spliterator
        Spliterator<String> spliterator = words.spliterator();
        Stream<String> stream = StreamSupport.stream(spliterator, false);

        System.out.println("Sequential processing:");
        stream.forEach(System.out::println);

        // Parallel processing
        Spliterator<String> parallelSplit = words.spliterator();
        Stream<String> parallelStream = StreamSupport.stream(parallelSplit, true);

        System.out.println("Parallel processing:");
        parallelStream.forEach(System.out::println);
    }
}

The example demonstrates how Spliterators enable both sequential and parallel stream processing. The second parameter to StreamSupport.stream determines if the stream should be parallel. The Spliterator handles the underlying data access in both cases.

Spliterator for Primitive Types

Java provides specialized Spliterators for primitive types: Spliterator.OfInt, Spliterator.OfLong, and Spliterator.OfDouble. These avoid boxing overhead when processing primitive values. This example demonstrates using an OfInt Spliterator.

Main.java
package com.zetcode;

import java.util.Arrays;
import java.util.Spliterator;
import java.util.stream.IntStream;
import java.util.stream.StreamSupport;

public class Main {

    public static void main(String[] args) {

        int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8};

        Spliterator.OfInt intSpliterator = Arrays.spliterator(numbers);

        System.out.println("Characteristics: " + intSpliterator.characteristics());
        System.out.println("Estimated size: " + intSpliterator.estimateSize());

        // Process elements
        System.out.println("Elements:");
        intSpliterator.forEachRemaining((int value) ->
                System.out.println("Value: " + value));

        // Create IntStream from Spliterator
        Spliterator.OfInt streamSpliterator = Arrays.spliterator(numbers);
        IntStream intStream = StreamSupport.intStream(streamSpliterator, true);

        System.out.println("Sum: " + intStream.sum());
    }
}

This example shows the specialized OfInt Spliterator working with an int array. The forEachRemaining method takes an IntConsumer, avoiding boxing. We also demonstrate creating an IntStream from the Spliterator for parallel processing of the primitive values.

Source

Java Spliterator Documentation

In this article, we've covered the essential aspects of the Java Spliterator interface with practical examples. The Spliterator is a powerful tool for traversing and partitioning data sources, especially for parallel processing with the Stream API. Understanding Spliterator helps in creating efficient, parallel-ready data processing code.

Author

My name is Jan Bodnar, and I am a dedicated programmer with many years of experience in the field. I began writing programming articles in 2007 and have since authored over 1,400 articles and eight e-books. With more than eight years of teaching experience, I am committed to sharing my knowledge and helping others master programming concepts.

List all Java tutorials.