How do you collect a Stream into a List, and what is the difference between Collectors.toList(), Collectors.toUnmodifiableList(), and Stream.toList()?
answer
- toList() = no mutability/type guarantee (today an ArrayList)
- toUnmodifiableList() = immutable + rejects null (Java 10)
- Stream.toList() = terminal op, unmodifiable, allows null (Java 16)
- Unmodifiable means structure frozen, not the elements
- toCollection(ArrayList::new) when you need a specific mutable type
basics
~10 sUse stream.collect(Collectors.toList()) to get a List. toUnmodifiableList() gives a List you cannot change (add/remove throws). Since Java 16, stream.toList() is a shorter way to get an unmodifiable list.
solid answer
~40 sCollectors.toList() collects stream elements into a List, but it gives no guarantee about the concrete type or mutability — historically an ArrayList you can modify, though you should not rely on that. Collectors.toUnmodifiableList() (Java 10+) produces an immutable List whose add/remove/set throw UnsupportedOperationException, and it rejects null elements. Java 16 added the terminal operation Stream.toList(), the most concise option, which also returns an unmodifiable list but, unlike toUnmodifiableList(), permits null elements. Prefer Stream.toList() for new code when you just need a read-only result; use Collectors.toList() inside collect chains where you need a mutable list or are combining with other collectors (e.g. groupingBy downstream).
code
java · 11 linesList<String> names = people.stream()
.map(Person::name)
.collect(Collectors.toList()); // mutable, unspecified type
List<String> ro = people.stream()
.map(Person::name)
.collect(Collectors.toUnmodifiableList()); // immutable, rejects null
List<String> shortest = people.stream()
.map(Person::name)
.toList(); // Java 16+, unmodifiable, allows nullgo deeper
Knows stream.collect(Collectors.toList()) returns a List and that toUnmodifiableList()/Stream.toList() give a read-only list.
Articulates that toList() makes no mutability/type guarantee, knows the null-handling difference between Stream.toList() and toUnmodifiableList(), and reaches for toCollection for a specific type.
Recommends Stream.toList() for read-only results, explains version availability (10/16), and warns against code that relies on the ArrayList implementation detail or on mutating the result.
Frames the choice in terms of API contracts and defensive copying, sets team conventions (prefer immutable results), and reasons about null-tolerance and downstream-collector composition implications.
## What a Collector is The **Java Stream API** lets you process sequences of data with a pipeline: a *source* (e.g. `list.stream()`), zero or more *intermediate operations* (`map`, `filter`), and exactly one *terminal operation* that produces a result. **`collect`** is a terminal operation that accumulates elements into a mutable container. The thing passed to `collect` is a **`Collector`** — a recipe describing how to create a container, add elements to it, and finish. The **`Collectors`** utility class provides ready-made collectors via static factory methods. ## Collecting into a List ```java List<String> names = people.stream() .map(Person::name) .collect(Collectors.toList()); ``` `Collectors.toList()` makes **no contract** about the concrete `List` type or whether it is mutable. In current OpenJDK it returns an `ArrayList`, so you *can* mutate it — but the Javadoc explicitly says callers must not depend on that. Relying on it is a portability bug. ## Unmodifiable variants - **`Collectors.toUnmodifiableList()`** (added in Java 10) returns an **immutable** `List`. Calling `add`, `remove`, `set`, or `clear` throws `UnsupportedOperationException`. It also **rejects `null`** elements — a `null` in the stream causes a `NullPointerException` while collecting. - **`Stream.toList()`** (added in Java 16) is a *terminal operation directly on `Stream`*, not a collector. `stream.toList()` is shorthand for `stream.collect(Collectors.toUnmodifiableList())` with one key difference: it **allows `null` elements**. It returns an unmodifiable list. "Unmodifiable" means you cannot structurally change the returned list. It does not deep-freeze the elements themselves — if the elements are mutable objects, those can still change. ## Which to use - Need a quick read-only list from a stream? Prefer **`Stream.toList()`** — shortest and clearly read-only. - Need to guarantee immutability *and* forbid nulls, or you are inside a `collect(...)` chain (for example as a downstream collector of `groupingBy`)? Use **`Collectors.toUnmodifiableList()`**. - Need a genuinely mutable list you will keep modifying? Use **`Collectors.toList()`** and ideally copy into a `new ArrayList<>(...)` to be explicit, or use `Collectors.toCollection(ArrayList::new)`. ## Sets The parallel collectors exist for sets: `Collectors.toSet()` (no order/mutability guarantee, typically a `HashSet`) and `Collectors.toUnmodifiableSet()` (immutable, rejects nulls, removes duplicates by `equals`/`hashCode`).
- What happens if the stream contains a null and you call Collectors.toUnmodifiableList()?It throws NullPointerException while collecting, because the unmodifiable collectors reject null elements. Stream.toList() would accept the null instead.
- How do you collect into a specific List implementation like LinkedList?Use Collectors.toCollection(LinkedList::new), which lets you supply the exact collection type via a constructor reference.
saying these in an interview costs you the question
- Claiming Collectors.toList() is guaranteed immutable — it is not
- Claiming Collectors.toList() is guaranteed to return ArrayList you may mutate — implementation detail, not contract
- Thinking Stream.toList() and Collectors.toUnmodifiableList() are identical (they differ on nulls)
- Believing 'unmodifiable' deep-freezes the contained objects