skip to content

Why do IntStream, LongStream, and DoubleStream exist when you already have Stream<Integer>? What problem do they solve?

level: juniorimportance: must knowfreq 70%

answer

  1. Stream<Integer> = boxed objects; IntStream = raw values
  2. no boxing/unboxing -> less allocation, less GC, better cache
  3. extra numeric ops: sum/average/min/max/summaryStatistics
  4. three specializations only: Int/Long/Double
  5. cross with mapToInt / boxed()

basics

~20 s

A Stream<Integer> stores each number as a wrapped Integer object, which costs extra memory and time. Primitive streams (IntStream, LongStream, DoubleStream) hold raw int/long/double values directly, avoiding that wrapping and giving handy numeric operations like sum() and average().

solid answer

~40 s

Stream<Integer> is a stream of objects: every int must be wrapped in an Integer (boxing) to live on the heap, and unwrapped (unboxing) to do arithmetic. For large numeric pipelines that boxing means extra allocations, more garbage, cache misses, and slower iteration. IntStream/LongStream/DoubleStream are specialized primitive streams that carry raw int/long/double values with no wrapper objects, so there is no boxing overhead. They also expose numeric terminal operations the object stream lacks — sum(), average(), min(), max(), summaryStatistics() — and factory methods like range()/rangeClosed(). You move between worlds with mapToInt/mapToObj and boxed(). So you reach for primitive streams whenever you process many numbers and want both performance and a cleaner numeric API.

code

java · 12 lines
java
// Object stream: every length is boxed into an Integer
List<String> words = List.of("a", "bb", "ccc");
int total1 = words.stream()
                  .map(String::length)        // Stream<Integer> (boxing)
                  .reduce(0, Integer::sum);    // unbox to add

// Primitive stream: raw ints, no boxing, built-in sum()
int total2 = words.stream()
                  .mapToInt(String::length)    // IntStream
                  .sum();                       // numeric terminal op

System.out.println(total1 == total2); // true — same result, cleaner & cheaper

go deeper

for a junior

Knows Stream<Integer> wraps each number in an object and that IntStream avoids that, and can name sum()/average() as built-in numeric ops.

for a middle

Articulates the concrete costs of boxing (allocation, GC, cache, repeated unboxing) and can convert between object and primitive streams with mapToInt/boxed.

for a senior

Reasons about when boxing actually matters (size, hot path), reaches for summaryStatistics for one-pass stats, and treats mapToInt(...).sum() as idiomatic over reduce.

for a principal

Frames the trade-off in API-design terms — why the JDK chose three specializations not a full matrix, the cost model, and when micro-optimizing a stream is premature vs. justified by profiling.

## The setup: what a Stream is The Java **Streams API** (since Java 8) lets you express a pipeline of operations over a sequence of elements — `filter`, `map`, `reduce`, `collect`, etc. A `Stream<T>` is a stream of **objects** of type `T`. `T` must be a reference type (a class), never a primitive — you cannot write `Stream<int>`. ## Primitives vs. objects, and 'boxing' Java has eight **primitive types** (`int`, `long`, `double`, `boolean`, …) that are not objects: an `int` is just 32 bits of value, stored compactly (e.g. inline in an array). For every primitive there is a **wrapper class** (`Integer`, `Long`, `Double`, …) that is a real object living on the heap with a header and a field holding the value. - **Boxing** = converting a primitive into its wrapper object, e.g. `int 5` → `Integer.valueOf(5)`. - **Unboxing** = the reverse, e.g. `Integer` → `int` via `intValue()`. Java does this automatically (**autoboxing**) so you rarely see it, but it still happens at runtime. ## Why Stream<Integer> is expensive Because generics only work over reference types, a stream of numbers as `Stream<Integer>` forces every number to be an `Integer` object. Concretely that means: 1. **Allocation / memory:** each `Integer` is a heap object (typically 16 bytes) versus 4 bytes for a raw `int`. A million numbers becomes a million tiny objects. 2. **Garbage-collection pressure:** all those short-lived wrappers must later be collected. 3. **Indirection / cache misses:** the stream holds references (pointers) to objects scattered on the heap rather than a tight block of values, hurting CPU cache locality. 4. **Repeated unboxing:** any arithmetic (`a + b`) must unbox back to `int`, do the math, and possibly re-box the result. For a numeric pipeline of meaningful size, this overhead is real and measurable. ## The fix: specialized primitive streams The JDK ships three primitive-specialized stream interfaces — **`IntStream`**, **`LongStream`**, **`DoubleStream`** — that carry raw `int`/`long`/`double` values with **no wrapper objects at all**. No boxing, no extra allocation, values stored compactly. (There is intentionally no `CharStream`/`ShortStream`/`BooleanStream`; you use `IntStream` for those, since `char`/`short`/`byte` widen to `int`.) They bring two benefits: - **Performance:** elimination of boxing/unboxing and the allocations it causes. - **A richer numeric API:** operations that only make sense for numbers are built in — `sum()`, `average()` (returns `OptionalDouble`), `min()`/`max()` (return `OptionalInt`/`OptionalLong`/`OptionalDouble`), and `summaryStatistics()` which returns an `IntSummaryStatistics` (count, sum, min, max, average in one pass). `Stream<Integer>` has none of these; you'd have to `reduce` or `collect` manually. They also add numeric factories: `IntStream.range(0, n)` / `rangeClosed(1, n)` generate sequences without materializing a collection. ## Moving between the two worlds - **Object stream → primitive stream:** `mapToInt` / `mapToLong` / `mapToDouble` (e.g. `strings.stream().mapToInt(String::length)`). - **Primitive stream → object stream:** `boxed()` (re-wraps each primitive back into its wrapper so you can `collect(toList())`), or `mapToObj` to map each primitive to an arbitrary object. - **Between primitive streams:** `asLongStream()` / `asDoubleStream()` widen an `IntStream` to a `LongStream`/`DoubleStream`. ## When to use which Use a primitive stream whenever you are genuinely working with numbers — counting, summing, averaging, indexing with `range`. Use `Stream<Integer>` only when you specifically need the elements as objects (e.g. to put them in a `List<Integer>`, or pass to generic code). The everyday rule: prefer `mapToInt(...).sum()` over `map(...).reduce(0, Integer::sum)`.

  • If primitive streams are faster, why not always use them?
    Because not every pipeline is numeric. When you need the elements as objects (e.g. collecting into a List<Integer>, or feeding generic APIs), an object stream is the right tool. Primitive streams shine specifically for numeric workloads — summing, averaging, ranges — where boxing would otherwise dominate.
  • Which primitive stream would you use for char or byte?
    IntStream. There are only three specializations (Int/Long/Double); char, short, and byte all widen to int, so you process them as an IntStream (e.g. "abc".chars()).

saying these in an interview costs you the question

  • Claiming Stream<Integer> and IntStream perform identically — boxing has real cost.
  • Thinking there is a CharStream or BooleanStream — there isn't; use IntStream.
  • Saying you can write Stream<int> — generics only accept reference types.
  • Believing primitive streams are only about speed and ignoring the richer numeric API.

context