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.
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:
IntStream: Represents a sequence of primitiveintvalues (also handlesbyte,short, andchar).LongStream: Represents a sequence of primitivelongvalues.DoubleStream: Represents a sequence of primitivedoublevalues (also handlesfloat).
┌────────────────────────┐
│ 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]
OptionalIntdoes NOT possess a.get()method. Calling.get()on anOptionalIntcauses 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 Stream | Target Stream | Transformation Method |
|---|---|---|
Stream<T> | IntStream | stream.mapToInt(ToIntFunction<T>) |
Stream<T> | LongStream | stream.mapToLong(ToLongFunction<T>) |
Stream<T> | DoubleStream | stream.mapToDouble(ToDoubleFunction<T>) |
IntStream | Stream<Integer> | intStream.boxed() |
IntStream | Stream<U> | intStream.mapToObj(IntFunction<U>) |
IntStream | DoubleStream | intStream.asDoubleStream() |
IntStream | LongStream | intStream.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
- Splitting (
trySplit): The source is recursively split into sub-ranges by itsSpliterator. - Execution: Leaf subtasks are executed concurrently on worker threads in
ForkJoinPool.commonPool(). - 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 (likeIterator.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
| Characteristic | Bit Constant | Impact on Stream Optimization |
|---|---|---|
ORDERED | Spliterator.ORDERED | Stream possesses an encounter order; operations like findFirst(), limit() must preserve sequence. |
DISTINCT | Spliterator.DISTINCT | Stream contains no duplicate elements; distinct() is optimized away as a no-op. |
SORTED | Spliterator.SORTED | Stream is already sorted; sorted() is a no-op. |
SIZED | Spliterator.SIZED | Exact size is known before traversal; collectors can pre-allocate array buffers. |
NONNULL | Spliterator.NONNULL | Guaranteed to contain no null elements. |
IMMUTABLE | Spliterator.IMMUTABLE | Data source cannot be structurally modified during traversal. |
CONCURRENT | Spliterator.CONCURRENT | Data source can be safely modified concurrently without external synchronization. |
SUBSIZED | Spliterator.SUBSIZED | All 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
forEachormap). 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
Examine the following code using IntStream numeric ranges:
What is printed when this code is executed?int s1 = IntStream.range(1, 5).sum();
int s2 = IntStream.rangeClosed(1, 5).sum();
System.out.println(s1 + " " + s2);
Given the following code snippet:
What is the result?List<String> words = List.of("Java", "SE", "21");
int sum = words.stream()
.mapToInt(String::length)
.sum();
System.out.println(sum);
Consider the following parallel stream snippet:
Which statement accurately describes the execution behavior of this code?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 of the following method invocations on an OptionalInt will cause a compile-time error?