How do primitive streams (IntStream/LongStream) and primitive collections avoid boxing compared to Stream<Integer> and List<Integer>?
answer
- Generics need reference types → List<Integer>/Stream<Integer> box
- IntStream/LongStream/DoubleStream carry raw primitives
- mapToInt unboxes once, pipeline stays primitive; boxed() re-boxes
- Primitive collections (fastutil/Eclipse) store int[] internally
- Wins on large/hot data: less allocation + better cache locality
basics
~20 sStream<Integer> and List<Integer> store and process boxed Integer objects, allocating one per element. IntStream and primitive collections (like fastutil's IntArrayList) hold raw ints, so no wrapper objects are created and there's no per-element GC pressure.
solid answer
~40 sA Stream<Integer> carries object references, so every element is a boxed Integer; operations like map, filter, and reduce shuffle those objects around, and intermediate results may re-box. IntStream (and LongStream, DoubleStream) is specialized to carry raw primitives end to end — its operations take IntUnaryOperator, IntPredicate, etc., never boxing. So `list.stream().mapToInt(Integer::intValue).sum()` unboxes once into a primitive pipeline and stays primitive. The standard JDK collections can't hold primitives (generics need reference types), so List<Integer> boxes every element and stores an array of pointers to scattered heap objects — bad for memory and cache locality. Primitive-specialized libraries (Eclipse Collections IntList, fastutil IntArrayList, Trove) store an int[] internally, giving compact, contiguous, allocation-free storage with list/map/set semantics. On hot paths over large datasets these eliminate both the allocation churn and the indirection.
go deeper
Knows IntStream exists and avoids boxing versus Stream<Integer>, and that List<Integer> stores objects.
Explains the once-at-the-boundary unboxing with mapToInt, why generics force boxing, and names primitive sum/average terminals.
Weighs cache locality and allocation rate, knows the boxed()/mapToObj boundary pitfalls, and chooses primitive-specialized collections deliberately with profiling.
Decides library/dependency strategy (fastutil vs Eclipse vs JDK), sets where primitive collections are warranted, and balances API ergonomics, footprint, and Valhalla's future against custom collection complexity.
## The problem with the standard generic pipeline Generics in Java require **reference types** — you cannot write `Stream<int>` or `List<int>`, only `Stream<Integer>` / `List<Integer>`. That means: - **`List<Integer>`** stores an `Object[]` (really `Integer[]`) of *pointers*. Each element is a separate heap object with a header. Adding an int boxes it; reading it as an int unboxes it. Memory blows up (~16 bytes/element + pointer vs 4 bytes for an int) and iteration chases pointers to scattered objects, hurting CPU **cache locality**. - **`Stream<Integer>`** is a pipeline of object references. `map`, `filter`, `reduce` operate on `Integer` objects; arithmetic inside a lambda unboxes and re-boxes, allocating intermediate wrappers. ## Primitive streams The JDK provides three specialized streams: **`IntStream`, `LongStream`, `DoubleStream`**. They carry *raw primitives*, not objects. Their operations use primitive functional interfaces (`IntUnaryOperator`, `IntPredicate`, `IntBinaryOperator`) so nothing is boxed inside the pipeline. They also offer primitive-friendly terminals: `sum()`, `average()`, `min()`, `max()`, `summaryStatistics()` — none of which box. Bridges between the two worlds: ```java // Object stream → primitive stream (unbox once) int total = list.stream().mapToInt(Integer::intValue).sum(); // primitive → object stream when you truly need objects Stream<Integer> boxed = IntStream.range(0, n).boxed(); ``` Key idea: `mapToInt` unboxes **once** at the boundary, then the whole pipeline is primitive. Compare with `list.stream().reduce(0, Integer::sum)`, which keeps boxing. ## Primitive-specialized collections When you need collection *semantics* (a growable list, a map, a set) over primitives without boxing, use a primitive-specialized library: - **fastutil** — `IntArrayList`, `Int2ObjectHashMap`, `LongOpenHashSet`, etc. - **Eclipse Collections** — `IntList`, `MutableIntList`, `IntObjectMap`. - **Trove** (`TIntArrayList`) — older but still used. Internally these store an `int[]` (or `long[]`), so: - No wrapper objects, no per-element allocation. - Contiguous memory → far better cache behavior on iteration. - Much smaller footprint for large datasets. The trade-off: an extra dependency, a non-`java.util` API, and you can't put them directly where a `List<Integer>` is expected without bridging. ## When it matters These choices matter on **hot paths over large data**: numeric aggregations, tight inner loops, high-throughput per-request processing. For small or cold collections, `List<Integer>` is perfectly fine and more idiomatic. Profile first; reach for primitive collections when allocation rate or cache misses show up. ## Common boxing leaks even with primitive streams - `IntStream.boxed()` re-boxes — only use it when you genuinely need objects. - `collect(Collectors.toList())` on a boxed stream stores Integers. - `Map<Integer, ...>` keys box on every `get`/`put`. - `mapToObj` deliberately leaves the primitive world; know when you're crossing the boundary.
- What's the difference between `mapToInt`, `mapToObj`, and `boxed` on streams?mapToInt converts an object/primitive stream to an IntStream (unboxing at the boundary), keeping the rest primitive. mapToObj goes from a primitive stream back to a Stream<T> by producing objects. boxed is the special case mapToObj(Integer::valueOf) that turns an IntStream into Stream<Integer>. Use mapToInt to stay primitive; use boxed/mapToObj only when you truly need objects.
- Why is `Map<Integer, V>` a boxing hazard on a hot path?Every key operation (get/put/containsKey) boxes the int argument into an Integer, and the map stores Integer keys. On a hot lookup path this allocates and chases pointers. A primitive-keyed map like fastutil's Int2ObjectOpenHashMap stores an int[] of keys and avoids it.
saying these in an interview costs you the question
- Thinking List<Integer> and int[] have the same memory/perf profile
- Believing Stream<Integer>.reduce(0, Integer::sum) avoids boxing
- Using IntStream.boxed() then claiming the pipeline is boxing-free
- Reaching for fastutil on small/cold collections where it adds complexity for no measurable gain