skip to content

Primitive Streams

IntStream, LongStream and DoubleStream avoid the boxing tax of Stream<Integer> and add numeric terminals like sum, average and summaryStatistics. Knowing boxed(), mapToObj and the primitive Optionals is what makes them usable in practice.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

6

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

open as a page

How do IntStream.range and IntStream.rangeClosed differ, and when would you use each?

level: juniorimportance: should knowfreq 55%

basics

~10 s

Both generate a stream of consecutive ints. range(start, end) excludes the end value; rangeClosed(start, end) includes it. So range(1, 5) gives 1,2,3,4 and rangeClosed(1, 5) gives 1,2,3,4,5.

open as a page

Explain boxed(), mapToObj, and the asLongStream()/asDoubleStream() conversions. How do you move between primitive streams and object streams?

level: middleimportance: should knowfreq 48%

basics

~20 s

boxed() turns an IntStream into a Stream<Integer> by wrapping each value, so you can collect into a List. mapToObj turns each primitive into any object you choose. asLongStream()/asDoubleStream() widen an IntStream to a LongStream/DoubleStream. To go from objects to primitives, use mapToInt/mapToLong/mapToDouble.

open as a page

What does summaryStatistics() return on a primitive stream, and why might you prefer it over calling sum(), min(), max(), and average() separately?

level: middleimportance: should knowfreq 50%

basics

~20 s

summaryStatistics() returns one object (e.g. IntSummaryStatistics) holding the count, sum, min, max, and average computed in a single pass over the stream. Calling sum(), min(), max(), average() separately would consume the stream multiple times — but a stream can only be consumed once.

open as a page

Why do IntStream.min(), average(), etc. return OptionalInt/OptionalDouble instead of Optional<Integer> or a plain value?

level: seniorimportance: should knowfreq 38%

basics

~20 s

A stream might be empty, so there may be no minimum or average to return. Optional represents 'maybe a value'. The primitive variants OptionalInt/OptionalDouble exist so the result stays unboxed — Optional<Integer> would force a wrapper object, defeating the point of using a primitive stream.

open as a page

When you sum a large IntStream the result can be wrong even though no exception is thrown. Why, and how does the design of primitive streams shape how you'd prevent it?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

IntStream.sum() returns an int, which is only 32 bits. If the total exceeds about 2.1 billion it silently wraps around (overflows) to a wrong, possibly negative number — no error is thrown. Fix it by widening first with asLongStream().sum(), or mapToLong, so the total is a 64-bit long.

open as a page