8.4 Primitive Streams and Parallel Stream Processing

Key Takeaways

  • Primitive streams (IntStream, LongStream, DoubleStream) avoid object boxing overhead and provide specialized numerical operations including sum, average, range, and summaryStatistics.
  • Bridging between object and primitive streams requires explicit mapping methods (mapToInt, mapToObj, boxed) and specialized functional interfaces (IntFunction, ToIntFunction, IntUnaryOperator).
  • Parallel streams split data across threads in ForkJoinPool.commonPool() using Spliterator decomposition, maximizing throughput for large, CPU-intensive, stateless operations.
  • Parallel stream operations must adhere strictly to non-interference and statelessness; mutating shared mutable state from within parallel operations causes race conditions and data corruption.
Last updated: September 2026

Primitive Streams and Parallel Stream Processing

While reference streams (Stream<T>) provide a uniform object-oriented abstraction, processing millions of numeric values through wrapper types (like Integer, Long, or Double) incurs substantial memory allocation and boxing/unboxing overhead. To achieve near-native performance, Java provides primitive specialized streams: IntStream, LongStream, and DoubleStream.

Furthermore, Java's Stream API allows seamless parallelization across multi-core processors via parallelStream() and .parallel(). For the Java SE 21 Developer (1Z0-830) exam, candidates must understand primitive creation, conversions, numerical aggregations, Spliterator mechanics, and the strict concurrency rules governing parallel stream execution.


1. Primitive Stream Architecture

Java provides three specialized primitive stream interfaces in java.util.stream:

  1. IntStream: Represents a sequence of primitive int values (also handles byte, short, and char).
  2. LongStream: Represents a sequence of primitive long values.
  3. DoubleStream: Represents a sequence of primitive double values (also handles float).
                             ┌────────────────────────┐
                             │     BaseStream<T, S>   │
                             └───────────┬────────────┘
                  ┌──────────────────────┼──────────────────────┐
                  │                      │                      │
        ┌─────────▼────────┐   ┌─────────▼────────┐   ┌─────────▼────────┐
        │    IntStream     │   │    LongStream    │   │   DoubleStream   │
        └──────────────────┘   └──────────────────┘   └──────────────────┘

Creating Primitive Streams

// 1. Static of() factory
IntStream is1 = IntStream.of(1, 2, 3, 4, 5);
DoubleStream ds1 = DoubleStream.of(1.5, 2.5, 3.5);

// 2. Numeric Ranges (IntStream & LongStream only - DoubleStream has no range methods)
IntStream exclusive = IntStream.range(1, 5);       // 1, 2, 3, 4 (5 is excluded)
IntStream inclusive = IntStream.rangeClosed(1, 5); // 1, 2, 3, 4, 5 (5 is included)

// 3. From Arrays
int[] nums = {10, 20, 30};
IntStream is2 = Arrays.stream(nums);

// 4. Random Primitives
IntStream randomInts = new Random().ints(5, 1, 100); // 5 ints between 1 and 99

// 5. From CharSequence / String
IntStream chars = "Java 21".chars(); // IntStream of UTF-16 char codes

2. Primitive Operations & Summary Statistics

Primitive streams include specialized numeric terminal operations not found on generic Stream<T>:

IntStream stream = IntStream.of(10, 20, 30, 40, 50);

// Direct numeric reduction (no Comparator needed!)
int sum = IntStream.of(1, 2, 3).sum(); // 6
OptionalDouble avg = IntStream.of(10, 20, 30).average(); // OptionalDouble[20.0]
OptionalInt min = IntStream.of(10, 20, 30).min();       // OptionalInt[10]
OptionalInt max = IntStream.of(10, 20, 30).max();       // OptionalInt[30]

// Single-pass Summary Statistics
IntSummaryStatistics stats = IntStream.of(10, 20, 30, 40, 50).summaryStatistics();
System.out.println("Count: " + stats.getCount()); // 5
System.out.println("Sum: " + stats.getSum());     // 150 (returns long)
System.out.println("Min: " + stats.getMin());     // 10
System.out.println("Max: " + stats.getMax());     // 50
System.out.println("Average: " + stats.getAverage()); // 30.0 (returns double)

Primitive Optional Types: OptionalInt, OptionalLong, OptionalDouble

Primitive optionals avoid boxing the contained numeric value. Note the method names for retrieving values:

OptionalInt optInt = IntStream.empty().max();

// Value extraction methods:
// optInt.getAsInt();    // Throws NoSuchElementException on empty!
int val = optInt.orElse(0); // 0
int computed = optInt.orElseGet(() -> 100);

[!WARNING] OptionalInt does NOT possess a .get() method. Calling .get() on an OptionalInt causes a compile-time error; you must call .getAsInt().


3. Conversions Between Object and Primitive Streams

Navigating between Stream<T> and primitive streams requires specific transformation methods:

List<String> words = List.of("Java", "SE", "21");

// Object Stream -> Primitive Stream
IntStream lengths = words.stream().mapToInt(String::length); // ToIntFunction
DoubleStream dStream = words.stream().mapToDouble(s -> s.length() * 1.5);
LongStream lStream = words.stream().mapToLong(s -> (long) s.length());

// Primitive Stream -> Object Stream (Boxed vs mapToObj)
Stream<Integer> boxedStream = IntStream.rangeClosed(1, 5).boxed(); // Wraps int into Integer
Stream<String> mappedObj = IntStream.rangeClosed(1, 3)
    .mapToObj(n -> "ID_" + n); // ["ID_1", "ID_2", "ID_3"]

// Primitive -> Primitive Conversions
DoubleStream fromIntToDouble = IntStream.of(1, 2, 3).asDoubleStream();
LongStream fromIntToLong = IntStream.of(1, 2, 3).asLongStream();

Transformation Summary Matrix

Source StreamTarget StreamTransformation Method
Stream<T>IntStreamstream.mapToInt(ToIntFunction<T>)
Stream<T>LongStreamstream.mapToLong(ToLongFunction<T>)
Stream<T>DoubleStreamstream.mapToDouble(ToDoubleFunction<T>)
IntStreamStream<Integer>intStream.boxed()
IntStreamStream<U>intStream.mapToObj(IntFunction<U>)
IntStreamDoubleStreamintStream.asDoubleStream()
IntStreamLongStreamintStream.asLongStream()

4. Parallel Streams Architecture & Execution

Parallel streams decompose stream workloads across available CPU cores using the Fork/Join Framework (ForkJoinPool.commonPool()).

// Creating a parallel stream
List<Integer> data = List.of(1, 2, 3, 4, 5, 6, 7, 8);

Stream<Integer> p1 = data.parallelStream();
Stream<Integer> p2 = data.stream().parallel();

// Converting back to sequential
Stream<Integer> seq = p2.sequential();

boolean isParallel = p1.isParallel(); // true

How Fork/Join Decomposition Works

  1. Splitting (trySplit): The source is recursively split into sub-ranges by its Spliterator.
  2. Execution: Leaf subtasks are executed concurrently on worker threads in ForkJoinPool.commonPool().
  3. Combining: Sub-results are combined up the fork-join tree using associative combiners.

5. Spliterator Mechanics and Characteristics

The Spliterator<T> ("splitable iterator") is the core engine backing all streams. It provides four essential methods:

  • boolean tryAdvance(Consumer<? super T> action): Steps forward one element (like Iterator.next()).
  • Spliterator<T> trySplit(): Partitions off a portion of elements to be processed by another thread in parallel.
  • long estimateSize(): Returns the estimated remaining element count.
  • int characteristics(): Returns a bitmask of stream characteristics.

Spliterator Characteristics Matrix

CharacteristicBit ConstantImpact on Stream Optimization
ORDEREDSpliterator.ORDEREDStream possesses an encounter order; operations like findFirst(), limit() must preserve sequence.
DISTINCTSpliterator.DISTINCTStream contains no duplicate elements; distinct() is optimized away as a no-op.
SORTEDSpliterator.SORTEDStream is already sorted; sorted() is a no-op.
SIZEDSpliterator.SIZEDExact size is known before traversal; collectors can pre-allocate array buffers.
NONNULLSpliterator.NONNULLGuaranteed to contain no null elements.
IMMUTABLESpliterator.IMMUTABLEData source cannot be structurally modified during traversal.
CONCURRENTSpliterator.CONCURRENTData source can be safely modified concurrently without external synchronization.
SUBSIZEDSpliterator.SUBSIZEDAll splits produced by trySplit() will also be SIZED.

6. Stateful vs. Stateless Operations in Parallel Processing

  • Stateless Operations (filter, map, flatMap): Subtasks operate completely independently across threads with zero synchronization overhead, achieving near-linear scaling.
  • Stateful Operations (sorted, distinct, limit, skip): Require barrier synchronization and cross-thread communication. In parallel pipelines, sorted() must buffer elements across all worker threads before merging, often running slower than sequential execution!

The unordered() Optimization

When processing a parallel stream where encounter order does not matter, calling .unordered() allows distinct(), limit(), and groupingByConcurrent() to execute dramatically faster without maintaining partition ordering constraints:

List<Integer> distinctItems = largeList.parallelStream()
    .unordered() // Removes ordering constraint
    .distinct()  // Much faster in parallel
    .toList();

7. Concurrency Hazards, Non-Interference, and Thread Safety

[!CAUTION] The Parallel Mutation Anti-Pattern: Never mutate shared state from inside a stream pipeline (e.g., inside forEach or map). Parallel streams execute across multiple concurrent worker threads; accessing non-thread-safe containers without synchronization causes race conditions, lost updates, and corrupted data.

// DANGEROUS / BUGGY CODE: Race condition on ArrayList
List<Integer> unsafeResult = new ArrayList<>();
IntStream.rangeClosed(1, 1000)
    .parallel()
    .filter(n -> n % 2 == 0)
    .forEach(unsafeResult::add); // CRASH or corrupted size < 500!

// CORRECT & THREAD-SAFE APPROACH: Use collect() or reduce()
List<Integer> safeResult = IntStream.rangeClosed(1, 1000)
    .parallel()
    .filter(n -> n % 2 == 0)
    .boxed()
    .toList(); // Perfectly thread-safe and deterministic count of 500
Loading diagram...
Primitive Conversions and Parallel ForkJoinPool Spliterator Decomposition
Test Your Knowledge

Examine the following code using IntStream numeric ranges:

int s1 = IntStream.range(1, 5).sum();
int s2 = IntStream.rangeClosed(1, 5).sum();
System.out.println(s1 + " " + s2);
What is printed when this code is executed?

A
B
C
D
Test Your Knowledge

Given the following code snippet:

List<String> words = List.of("Java", "SE", "21");
int sum = words.stream()
    .mapToInt(String::length)
    .sum();
System.out.println(sum);
What is the result?

A
B
C
D
Test Your Knowledge

Consider the following parallel stream snippet:

List<Integer> syncList = Collections.synchronizedList(new ArrayList<>());
IntStream.rangeClosed(1, 100)
    .parallel()
    .filter(n -> n % 10 == 0)
    .forEach(syncList::add);

System.out.println(syncList.size());
Which statement accurately describes the execution behavior of this code?

A
B
C
D
Test Your Knowledge

Which of the following method invocations on an OptionalInt will cause a compile-time error?

A
B
C
D