Explain boxed(), mapToObj, and the asLongStream()/asDoubleStream() conversions. How do you move between primitive streams and object streams?
answer
- mapToInt/Long/Double: objects -> primitives
- boxed(): primitives -> wrapper object stream (needed for toList)
- mapToObj: primitive -> any object (more general than boxed)
- asLongStream/asDoubleStream: widen only, no narrowing
- IntStream.collect(toList()) won't compile — box first
basics
~20 sboxed() 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.
solid answer
~50 sPrimitive and object streams are separate type families, so you convert explicitly. From an object stream Stream<T>, mapToInt/mapToLong/mapToDouble extract a primitive and give you an IntStream/LongStream/DoubleStream — no boxing downstream. To go back to objects: boxed() re-wraps each primitive into its wrapper (IntStream -> Stream<Integer>), which you need before collect(Collectors.toList()); or mapToObj(i -> ...) maps each primitive to an arbitrary object directly, which is more flexible than boxed() plus a map. Among primitive streams, asLongStream() and asDoubleStream() widen an IntStream (int -> long or double) — IntStream and LongStream have asDoubleStream, IntStream also has asLongStream; there is no narrowing version. A frequent mistake is calling .collect(toList()) straight on an IntStream — it won't compile because Collectors target object streams; insert boxed() first. Choose boxed() when you simply need the wrapper, mapToObj when you need to build a different object.
code
java · 16 lines// objects -> primitives -> back to a List<Integer>
List<Integer> evens = Stream.of("2", "4", "6")
.mapToInt(Integer::parseInt) // IntStream (no boxing in the pipeline)
.filter(n -> n % 2 == 0)
.boxed() // Stream<Integer>
.collect(Collectors.toList());
// primitive -> arbitrary object with mapToObj
List<String> labels = IntStream.rangeClosed(1, 3)
.mapToObj(i -> "item-" + i) // Stream<String>
.toList(); // [item-1, item-2, item-3]
// widen to avoid int overflow when summing
long big = IntStream.rangeClosed(1, 100_000)
.asLongStream() // LongStream
.sum(); // long resultgo deeper
Knows boxed() converts IntStream to Stream<Integer> and that you need it to collect into a List.
Distinguishes mapToInt (objects->primitives), boxed() (wrapper), and mapToObj (any object), and knows asLong/asDouble widen; can fix the IntStream.collect compile error.
Picks mapToObj vs boxed() deliberately, uses asLongStream to prevent int-sum overflow, and knows there is no narrowing conversion.
Reasons about the API design (separate type families, why widening-only conversions exist) and reviews numeric pipelines for boxing leaks and overflow risk.
## Two separate type families The Streams API has, in effect, two worlds: - **Object streams:** `Stream<T>` — elements are objects of reference type `T`. - **Primitive streams:** `IntStream`, `LongStream`, `DoubleStream` — elements are raw `int`/`long`/`double`, no wrappers. They are **distinct interfaces** with no inheritance between them, so you must convert explicitly when crossing. Here is the full toolbox. ## Object stream → primitive stream: mapToInt / mapToLong / mapToDouble These take a function that extracts a primitive from each object and hand you the matching primitive stream: ```java Stream<String> words = Stream.of("a", "bb", "ccc"); IntStream lengths = words.mapToInt(String::length); // each String -> its int length ``` From here, everything downstream is boxing-free (`sum()`, `average()`, etc.). This is the standard way to start a numeric computation from objects. ## Primitive stream → object stream: boxed() `boxed()` is the inverse: it **wraps** each primitive in its wrapper class, turning the primitive stream back into an object stream: - `IntStream.boxed()` → `Stream<Integer>` - `LongStream.boxed()` → `Stream<Long>` - `DoubleStream.boxed()` → `Stream<Double>` You need this whenever an API only accepts object streams. The classic case is **collecting into a List**: `Collectors.toList()` produces a `List<T>` and works on `Stream<T>`, **not** on `IntStream`. So: ```java List<Integer> list = IntStream.rangeClosed(1, 5) .boxed() // -> Stream<Integer> .collect(Collectors.toList()); // [1,2,3,4,5] ``` Without `boxed()`, `IntStream.collect(...)` exists but has a different three-argument supplier/accumulator/combiner signature and does **not** accept a `Collector`, so the call won't compile. (As of Java 16+ you can also use `.boxed().toList()`.) ## Primitive stream → object stream of arbitrary type: mapToObj `boxed()` only gives you the wrapper. When you want each primitive mapped to **some other object**, use `mapToObj`: ```java Stream<String> cells = IntStream.rangeClosed(1, 3) .mapToObj(i -> "row" + i); // Stream<String> ``` `mapToObj(f)` is strictly more general than `boxed()`: `boxed()` is essentially `mapToObj(Integer::valueOf)`. Use `mapToObj` to build labels, DTOs, or any non-wrapper object; use `boxed()` when the wrapper itself is what you want. ## Between primitive streams: asLongStream() / asDoubleStream() These **widen** a narrower primitive stream to a wider one (a lossless numeric promotion, like `int → long`): - `IntStream.asLongStream()` → `LongStream` (each `int` widened to `long`) - `IntStream.asDoubleStream()` → `DoubleStream` (each `int` widened to `double`) - `LongStream.asDoubleStream()` → `DoubleStream` There is **no narrowing** conversion (no `asIntStream()` on `LongStream`) because that could lose data; to narrow you'd `mapToInt(l -> (int) l)` explicitly and accept the truncation. Widening matters when, say, you sum `int`s but the total could overflow `int` — promote to `LongStream` first so `sum()` returns a `long`. ## Putting it together (mental map) ``` Stream<T> --mapToInt--> IntStream --asLongStream--> LongStream ^ | \ | boxed() / mapToObj | asDoubleStream +------------------------- + ---> DoubleStream ``` ## The most common pitfalls 1. **`IntStream.collect(Collectors.toList())` won't compile** — insert `boxed()` first (or use `.boxed().toList()`). 2. **Using `map` where you need `mapToObj`** — on an `IntStream`, `map` must return an `int` (it stays an `IntStream`); to produce objects you must use `mapToObj`. 3. **Overflow when summing many/large ints** — `IntStream.sum()` returns an `int` and can overflow silently; `asLongStream().sum()` returns a `long`.
- Why won't IntStream.collect(Collectors.toList()) compile, and how do you fix it?Collectors.toList() returns a Collector for object streams, but IntStream.collect has a different (supplier, accumulator, combiner) signature that doesn't take a Collector. Fix it by calling boxed() first to get a Stream<Integer> (then .collect(toList()) or .toList()).
- When would you choose asLongStream() over leaving it as an IntStream?When summing or accumulating could overflow a 32-bit int. IntStream.sum() returns an int and overflows silently; asLongStream().sum() returns a long with far more headroom. It's a lossless widening, so it's a cheap safety measure for large datasets.
saying these in an interview costs you the question
- Calling .collect(Collectors.toList()) directly on an IntStream and expecting it to compile.
- Confusing boxed() (wrapper only) with mapToObj (any object).
- Expecting an asIntStream() narrowing method — it doesn't exist; cast in mapToInt.
- Trying to use map on an IntStream to produce Strings — map stays primitive; use mapToObj.