8.4 Primitive Streams (`IntStream`, `LongStream`, `DoubleStream`) and Numeric Statistics
Key Takeaways
Primitive streams (IntStream, LongStream, DoubleStream) eliminate boxing and unboxing overhead, bypassing object allocation and pointer indirection for high-throughput numeric workloads.
Numeric ranges are generated via range(start, end) (exclusive upper bound) and rangeClosed(start, end) (inclusive upper bound); DoubleStream does not support range generation.
Conversions between object streams and primitive streams are achieved using mapToInt(), mapToLong(), mapToDouble(), boxed(), and mapToObj().
Primitive stream terminal reductions include sum() (returning primitive number, 0 on empty), average() (returning OptionalDouble across all primitive streams), and min()/max() (returning primitive optionals).
summaryStatistics() calculates count, sum, min, average, and max in a single traversal, returning an IntSummaryStatistics, LongSummaryStatistics, or DoubleSummaryStatistics instance.
8.4 Primitive Streams (IntStream, LongStream, DoubleStream) and Numeric Statistics
While generic Stream<T> handles reference types seamlessly, using it for numeric computations involves boxing primitive numbers into wrapper objects (Integer, Long, Double). Each boxed object introduces 16 to 24 bytes of heap memory overhead, cache line pollution, and CPU cycles spent on autoboxing and unboxing. To eliminate these performance bottlenecks, the java.util.stream package provides three specialized primitive stream interfaces: IntStream, LongStream, and DoubleStream.
The Three Primitive Stream Types
Java provides primitive stream specializations for the three most common primitive numeric representations:
IntStream: 32-bit signed integers. Also handles narrower integral primitives (byte,short, andchar) through implicit widening.LongStream: 64-bit signed integers.DoubleStream: 64-bit IEEE 754 floating-point numbers. Also handles 32-bitfloatprimitives through implicit widening.
Note
Java does not provide ByteStream, ShortStream, CharStream, FloatStream, or BooleanStream. Integral types narrower than int are always processed as IntStream, float is processed as DoubleStream, and booleans are represented using predicates or standard reference streams.
Primitive Stream Creation and Numeric Ranges
Primitive streams can be instantiated using explicit values, arrays, generator functions, or contiguous numeric ranges:
1. Explicit Values and Arrays
IntStream s1 = IntStream.of(1, 2, 3, 4, 5);
IntStream s2 = IntStream.of(100); // Single element
LongStream s3 = LongStream.of(10L, 20L, 30L);
DoubleStream s4 = DoubleStream.of(1.5, 2.5, 3.5);
int[] numbers = { 10, 20, 30 };
IntStream s5 = Arrays.stream(numbers); // Array source
2. Contiguous Ranges: range() vs. rangeClosed()
IntStream and LongStream provide static factory methods to generate sequential arithmetic progressions with step size 1:
range(startInclusive, endExclusive): Increments fromstartInclusiveup toendExclusive - 1.rangeClosed(startInclusive, endInclusive): Increments fromstartInclusiveup to and includingendInclusive.
// Generates 1, 2, 3, 4 (count = 4)
long countA = IntStream.range(1, 5).count();
// Generates 1, 2, 3, 4, 5 (count = 5)
long countB = IntStream.rangeClosed(1, 5).count();
Warning
The DoubleStream Range Absence Trap
Neither range() nor rangeClosed() exists on DoubleStream! Because floating-point numbers have an infinite number of real values between any two points and step sizes cannot default to 1 without precision ambiguity, DoubleStream.range(1.0, 5.0) does not exist and will fail compilation.
3. Iterators and Generators
// Infinite even numbers: 0, 2, 4, 6, ...
IntStream evens = IntStream.iterate(0, n -> n + 2);
// Finite 3-arg iterate (Java 9+): 10, 20, 30, 40, 50
IntStream tens = IntStream.iterate(10, n -> n <= 50, n -> n + 10);
// Random numbers via IntSupplier
IntStream randomDice = IntStream.generate(() -> (int)(Math.random() * 6) + 1).limit(3);
Conversions: Boxing, Mapping, and Bridging
Transitioning between generic object streams and primitive streams requires dedicated bridge methods:
1. Object Stream to Primitive Stream
To convert a Stream<T> to a primitive stream, use the mapTo* family of intermediate operations:
mapToInt(ToIntFunction<? super T>)->IntStreammapToLong(ToLongFunction<? super T>)->LongStreammapToDouble(ToDoubleFunction<? super T>)->DoubleStream
List<String> items = List.of("apple", "banana", "pear");
IntStream lengths = items.stream().mapToInt(String::length);
2. Primitive Stream to Object Stream
To convert a primitive stream back into an object stream, use boxed() or mapToObj():
boxed(): Converts each primitive into its corresponding wrapper object (IntStream->Stream<Integer>).mapToObj(IntFunction<? extends U>): Transforms each primitive into an arbitrary reference objectU.
IntStream numbers = IntStream.rangeClosed(1, 3);
// Boxed wrapper stream
Stream<Integer> boxedStream = numbers.boxed();
// Mapping to custom domain objects
Stream<String> labeled = IntStream.rangeClosed(1, 3)
.mapToObj(n -> "ID-" + n); // Stream<String>
3. Widening Inter-Primitive Conversions
Primitive streams support implicit widening conversions without manual casting:
IntStream.asLongStream()->LongStreamIntStream.asDoubleStream()->DoubleStreamLongStream.asDoubleStream()->DoubleStream
DoubleStream ds = IntStream.range(1, 5).asDoubleStream(); // Yields 1.0, 2.0, 3.0, 4.0
Narrowing conversions (e.g., LongStream to IntStream or DoubleStream to IntStream) are not provided as direct methods to prevent accidental precision loss; you must use explicit casts inside mapToInt(d -> (int) d).
Specialized Reductions: sum(), average(), min(), max()
Primitive streams provide dedicated mathematical reduction methods that avoid the overhead of general reduce() and custom comparators:
| Reduction Method | Return Type | Empty Stream Result | Key Characteristics |
|---|---|---|---|
sum() | int / long / double | 0 (or 0.0) | Returns primitive directly; never returns an Optional! |
average() | OptionalDouble | OptionalDouble.empty() | Always returns OptionalDouble, even for IntStream! |
min() | OptionalInt / OptionalLong / OptionalDouble | Optional*.empty() | Returns specialized primitive optional. |
max() | OptionalInt / OptionalLong / OptionalDouble | Optional*.empty() | Returns specialized primitive optional. |
int sum = IntStream.of(10, 20, 30).sum(); // 60
OptionalDouble avg = IntStream.of(10, 20, 30).average(); // OptionalDouble[20.0]
OptionalInt max = IntStream.of(10, 20, 30).max(); // OptionalInt[30]
// Empty stream behavior:
int emptySum = IntStream.empty().sum(); // 0
OptionalDouble emptyAvg = IntStream.empty().average(); // OptionalDouble.empty
OptionalInt emptyMin = IntStream.empty().min(); // OptionalInt.empty
Warning
The sum() vs. average() Return Type Trap
Notice the deliberate design asymmetry:
sum()returns a primitive value directly (int,long, ordouble). On an empty stream,sum()evaluates to0.average()returns anOptionalDoubleacross all primitive streams (IntStream.average(),LongStream.average(), andDoubleStream.average()). An average of integers can produce a floating-point number, and an average over an empty stream is mathematically undefined (division by zero).
Multi-Metric Aggregation: summaryStatistics()
Because streams adhere to the single-use rule, executing min(), max(), and average() on the same stream would require reading and recreating the stream multiple times. To solve this, primitive streams provide the summaryStatistics() terminal operation.
summaryStatistics() consumes the stream in a single pass and computes count, sum, min, average, and max simultaneously:
IntSummaryStatistics stats = IntStream.rangeClosed(1, 100).summaryStatistics();
System.out.println("Count: " + stats.getCount()); // 100
System.out.println("Sum: " + stats.getSum()); // 5050
System.out.println("Min: " + stats.getMin()); // 1
System.out.println("Average: " + stats.getAverage()); // 50.5
System.out.println("Max: " + stats.getMax()); // 100
Combining Statistics and Empty Stream Edge Cases
SummaryStatistics objects can also be combined across data partitions using combine():
IntSummaryStatistics stats1 = IntStream.rangeClosed(1, 50).summaryStatistics();
IntSummaryStatistics stats2 = IntStream.rangeClosed(51, 100).summaryStatistics();
stats1.combine(stats2); // Merges stats2 into stats1
System.out.println("Merged sum: " + stats1.getSum()); // 5050
Warning
summaryStatistics() on Empty Streams
When summaryStatistics() is called on an empty stream, getCount() returns 0, getSum() returns 0, and getAverage() returns 0.0. However, to preserve mathematical identity properties when combining statistics, getMin() returns Integer.MAX_VALUE (2147483647) and getMax() returns Integer.MIN_VALUE (-2147483648)!
Consider the following code snippet:
long count = IntStream.range(1, 5).count();
OptionalDouble avg = IntStream.rangeClosed(1, 5).average();
System.out.println(count + " " + avg.orElse(0.0));
What is printed to standard output when this code is executed?
5 2.5
4 2.5
4 3.0
5 3.0
What is the result of executing the following code on an empty IntStream?
int sum = IntStream.empty().sum();
OptionalDouble avg = IntStream.empty().average();
IntSummaryStatistics stats = IntStream.empty().summaryStatistics();
System.out.println(sum + " " + avg.isPresent() + " " + stats.getMin());
0 false 0
Throws a NoSuchElementException when stats.getMin() is called.
0 false 2147483647
0 true 0
Which of the following stream declarations or invocations fails to compile?
Stream<Integer> stream = IntStream.of(1, 2, 3).boxed();
DoubleStream stream = DoubleStream.range(1.0, 5.0);
DoubleStream stream = IntStream.range(1, 4).asDoubleStream();
IntStream stream = Stream.of("10", "20").mapToInt(Integer::parseInt);
Sections you finish are checked off in the contents.