skip to content

Why prefer IntStream/LongStream/DoubleStream over Stream<Integer> for numeric work, and what is the cost of getting it wrong?

level: juniorimportance: should knowfreq 55%

answer

  1. Stream<Integer> = objects on heap; IntStream = raw ints
  2. Boxing/unboxing = GC pressure, not a correctness bug
  3. mapToInt/mapToLong/mapToDouble to enter; boxed() to leave
  4. IntStream gives sum()/average()/summaryStatistics() for free
  5. Box late, unbox early

basics

~10 s

Stream<Integer> wraps every number in an Integer object (boxing), which wastes memory and time. IntStream keeps raw int values, so it is faster and has handy methods like sum() and average().

solid answer

~40 s

A Stream<Integer> stores each value as a heap-allocated Integer object. Iterating it constantly boxes and unboxes (converting between int and Integer), which creates garbage and adds overhead, especially in tight loops over large data. The primitive specializations IntStream, LongStream, and DoubleStream hold raw primitives instead, so they avoid that allocation. They also expose numeric operations the generic Stream lacks, like sum(), average(), min(), max(), and summaryStatistics(), which return correct numeric results without you reducing manually. Convert into a primitive stream early with mapToInt/mapToLong/mapToDouble, do the arithmetic there, and only box back (boxed()) if a downstream API truly needs objects. Getting it wrong is rarely a correctness bug; it is a performance and memory-pressure problem that shows up under load.

code

java · 12 lines
java
// Boxing-heavy: every element is an Integer object
int slow = list.stream().reduce(0, Integer::sum);

// Primitive: no boxing inside the pipeline
int fast = list.stream()
               .mapToInt(Integer::intValue)
               .sum();

IntSummaryStatistics stats = list.stream()
               .mapToInt(Integer::intValue)
               .summaryStatistics();
System.out.println(stats.getAverage()); // count/sum/min/max/avg in one pass

go deeper

for a junior

Knows Stream<Integer> boxes and IntStream does not, and can use mapToInt(...).sum().

for a middle

Explains the GC/allocation cost, uses summaryStatistics(), and knows average() returns OptionalDouble.

for a senior

Reasons about when boxing actually matters (hot paths, large N), applies box-late/unbox-early, and weighs readability vs. micro-optimization.

for a principal

Sets team guidance on primitive streams in performance-sensitive modules, understands the Integer cache and JIT escape analysis caveats, and avoids premature optimization where N is small.

## The vocabulary first - **Primitive**: a raw machine value in Java — `int`, `long`, `double`, `boolean`, etc. It lives directly on the stack or inline in an array, with no object header. - **Wrapper / boxed type**: the object form of a primitive — `Integer`, `Long`, `Double`. It is a full Java object living on the heap, carrying an object header (typically 12–16 bytes) plus the value. - **Boxing**: converting a primitive into its wrapper (`int 5` → `Integer.valueOf(5)`). **Unboxing** is the reverse (`Integer` → `int`). The compiler inserts these automatically ("autoboxing"), so they are easy to miss. - **Stream**: in Java's `java.util.stream`, a pipeline that carries a sequence of elements through operations like `map`, `filter`, `reduce`. ## The problem A generic `Stream<T>` can only carry **objects**, never primitives — generics in Java cannot be parameterized with `int`. So `Stream<Integer>` carries `Integer` objects. Every value that enters or leaves arithmetic gets boxed/unboxed: ```java Stream<Integer> s = list.stream(); // each element is an Integer object int total = s.reduce(0, Integer::sum); // each sum unboxes two Integers, boxes the result ``` For a few elements this is invisible. For millions, you allocate millions of short-lived `Integer` objects, which: 1. **Costs CPU** for each `valueOf`/`intValue` call. 2. **Costs memory** — each `Integer` is far larger than a 4-byte `int`. 3. **Pressures the garbage collector** — all that garbage must be collected, causing pauses. (Note: `Integer.valueOf` caches small values, typically −128..127, so tiny numbers reuse cached objects — but you cannot rely on this for general data.) ## The fix: primitive streams Java provides three primitive specializations: **`IntStream`**, **`LongStream`**, **`DoubleStream`**. They carry raw primitives — no boxing inside the pipeline. You enter them with `mapToInt`, `mapToLong`, `mapToDouble`, or factories like `IntStream.range(0, n)`: ```java int total = list.stream() .mapToInt(Integer::intValue) // now an IntStream of raw ints .sum(); // no boxing in the reduction ``` They also give you **numeric terminal operations the generic Stream lacks**: `sum()`, `average()` (returns `OptionalDouble`), `min()`, `max()`, and `summaryStatistics()` (count, sum, min, max, average in one pass). On a generic `Stream<Integer>` you would have to write `reduce` or `collect` manually. To go back to objects (when a downstream API needs them), call `.boxed()` — but do it as late as possible. ## The rule of thumb **Box late, unbox early.** Convert to a primitive stream as soon as you start doing arithmetic, stay primitive through the numeric work, and only `boxed()` back if an object-typed collector or API forces you. This is a performance/memory concern, not usually a correctness one — but in hot paths it matters a lot.

  • What does IntStream.average() return and why?
    OptionalDouble — because the stream could be empty, there is no meaningful average, so it returns an empty OptionalDouble rather than throwing or returning 0.
  • How do you turn an IntStream back into a Stream<Integer>?
    Call .boxed(), or .mapToObj(Integer::valueOf). Do it as late as possible to keep the pipeline primitive.

saying these in an interview costs you the question

  • Claiming Stream<Integer> and IntStream perform identically
  • Thinking boxing causes wrong answers rather than overhead
  • Forgetting IntStream.average() returns OptionalDouble, not double
  • Assuming you must reduce manually to sum a Stream<Integer> (you can mapToInt)

context