primitive streams
IntStream and friends, and the cost of boxing.
- chapter
- chapter 07.08
- reading time
- 6 min read
a Stream<Integer> is a stream of objects: every number is boxed into its own Integer on the heap. IntStream, LongStream and DoubleStream carry raw primitives instead, and they add the number-y terminal operations you actually want: sum, average, max, summaryStatistics.
convert with mapToInt / mapToLong / mapToDouble, and go back with boxed() or mapToObj.
the picture
you know how List<Integer> can't hold an int, so java quietly wraps each one in an Integer? a Stream<Integer> has the same problem, and in a stream it happens once per element, sometimes twice (once going in, once for every intermediate result).
think of shipping marbles. you can put every marble in its own little gift box with a label (boxing), or pour them straight into a tube (primitives). the gift boxes cost cardboard and somebody has to throw them away afterwards. that somebody is the garbage collector.
Integer is an array of references to separate objects (each 16 bytes on a 64-bit JVM, plus the 4-byte reference). an int[] is just the numbers, back to back.what boxing actually costs
summing a million ints, four ways. measured on jdk 25 on an arm mac: bytes allocated by the summing thread for one call, after warm-up.
boxed().reduce boxes every element and every partial sum: a million Integers each way, 16 bytes apiece. mapToInt on the same boxed list unboxes on the fly and allocates next to nothing.time follows the garbage. in the same run (best of 100 calls, timed with System.nanoTime, so treat it as rough, not a JMH result) the primitive sum took about 1.0 ms and the boxed one about 3.8 ms. the allocation numbers are the solid part: they don't move between runs.
the four kinds, and how to get between them
asLongStream), anything that could lose information needs a function you write (mapToInt(f)), and the only way back to objects is boxed() or mapToObj.import java.util.*;
import java.util.stream.*;
void main() {
IO.println(IntStream.range(0, 5).boxed().toList()); // end exclusive
IO.println(IntStream.rangeClosed(1, 5).sum()); // end inclusive
IO.println(IntStream.of(4, 8, 15, 16, 23, 42).summaryStatistics());
IO.println(IntStream.of(4, 8, 15).average()); // OptionalDouble
IO.println(IntStream.empty().average()); // nothing to average
List<String> words = List.of("fig", "kiwi", "avocado");
IO.println(words.stream().mapToInt(String::length).sum()); // objects → ints
IO.println(IntStream.range(0, 3).mapToObj(i -> "#" + i).toList()); // ints → objects
IO.println(IntStream.iterate(1, i -> i <= 100, i -> i * 3).boxed().toList());
}[0, 1, 2, 3, 4]
15
IntSummaryStatistics{count=6, sum=108, min=4, average=18.000000, max=42}
OptionalDouble[9.0]
OptionalDouble.empty
14
[#0, #1, #2]
[1, 3, 9, 27, 81]summaryStatistics() is the underrated one: count, sum, min, average and max in a single pass. and IntStream.range(0, n) is the stream version of for (int i = 0; i < n; i++).
"hi".chars().forEach(System.out::print);gotchas
why not just Stream<int>?+
because java generics only work with reference types: Stream<int> isn't legal, so the JDK had to write IntStream and friends by hand, three times over, plus the 30-odd primitive functional interfaces in java.util.function (see functional interfaces). it stopped at int, long and double, which is why there's no CharStream or FloatStream. project valhalla aims to fix the root problem by letting generics work over primitive-like value types. see valhalla.
Stream<Integer>boxes every element.IntStreamdoesn't.mapToInt/mapToLong/mapToDoubleto get primitive,boxed()/mapToObjto get back.- primitive streams add
sum,average,max,summaryStatistics, andrange. IntStream.sum()can overflow. widen withasLongStream()when the total might be big."text".chars()is anIntStreamof codes, not characters.