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.

Last updated: October 2026

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, and char) through implicit widening.
  • LongStream: 64-bit signed integers.
  • DoubleStream: 64-bit IEEE 754 floating-point numbers. Also handles 32-bit float primitives 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 from startInclusive up to endExclusive - 1.
  • rangeClosed(startInclusive, endInclusive): Increments from startInclusive up to and including endInclusive.
// 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>) -> IntStream
  • mapToLong(ToLongFunction<? super T>) -> LongStream
  • mapToDouble(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 object U.
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() -> LongStream
  • IntStream.asDoubleStream() -> DoubleStream
  • LongStream.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 MethodReturn TypeEmpty Stream ResultKey Characteristics
sum()int / long / double0 (or 0.0)Returns primitive directly; never returns an Optional!
average()OptionalDoubleOptionalDouble.empty()Always returns OptionalDouble, even for IntStream!
min()OptionalInt / OptionalLong / OptionalDoubleOptional*.empty()Returns specialized primitive optional.
max()OptionalInt / OptionalLong / OptionalDoubleOptional*.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:

  1. sum() returns a primitive value directly (int, long, or double). On an empty stream, sum() evaluates to 0.
  2. average() returns an OptionalDouble across all primitive streams (IntStream.average(), LongStream.average(), and DoubleStream.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)!

Test Your Knowledge

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?

A

5 2.5

B

4 2.5

C

4 3.0

D

5 3.0

Test Your Knowledge

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());
A

0 false 0

B

Throws a NoSuchElementException when stats.getMin() is called.

C

0 false 2147483647

D

0 true 0

Test Your Knowledge

Which of the following stream declarations or invocations fails to compile?

A

Stream<Integer> stream = IntStream.of(1, 2, 3).boxed();

B

DoubleStream stream = DoubleStream.range(1.0, 5.0);

C

DoubleStream stream = IntStream.range(1, 4).asDoubleStream();

D

IntStream stream = Stream.of("10", "20").mapToInt(Integer::parseInt);

Sections you finish are checked off in the contents.