skip to content

Collectors: to Collection/Map

toList, toSet and toMap, with toMap's duplicate-key IllegalStateException as the trap and the merge-function overload as the fix. Interviewers ask about that exception because grouping data by a non-unique key is such a common task.

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

questions

5

How do you collect a Stream into a List, and what is the difference between Collectors.toList(), Collectors.toUnmodifiableList(), and Stream.toList()?

level: juniorimportance: must knowfreq 72%

answer

  1. toList() = no mutability/type guarantee (today an ArrayList)
  2. toUnmodifiableList() = immutable + rejects null (Java 10)
  3. Stream.toList() = terminal op, unmodifiable, allows null (Java 16)
  4. Unmodifiable means structure frozen, not the elements
  5. toCollection(ArrayList::new) when you need a specific mutable type

basics

~10 s

Use 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 s

Collectors.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 lines
java
List<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 null

go deeper

for a junior

Knows stream.collect(Collectors.toList()) returns a List and that toUnmodifiableList()/Stream.toList() give a read-only list.

for a middle

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.

for a senior

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.

for a principal

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

context

open as a page

How does Collectors.toMap(keyMapper, valueMapper) work, and what does each function receive and produce?

level: juniorimportance: must knowfreq 68%

basics

~20 s

toMap takes two functions: a keyMapper that turns each element into the map key, and a valueMapper that turns each element into the value. The result is a Map built from those keys and values.

open as a page

Collectors.toMap throws IllegalStateException on duplicate keys. How do you resolve that, and what does the 3-argument merge-function overload do?

level: middleimportance: must knowfreq 63%

basics

~20 s

Add a third argument: a merge function. When two elements produce the same key, the merge function takes the existing value and the new value and returns the one to keep. For example (a, b) -> b keeps the last value.

open as a page

How do you control which concrete Map type Collectors.toMap returns (e.g. a TreeMap or LinkedHashMap), and what does the 4-argument Supplier overload require?

level: middleimportance: should knowfreq 48%

basics

~10 s

Use the four-argument toMap(keyMapper, valueMapper, mergeFunction, mapSupplier). The fourth argument is a supplier like TreeMap::new that creates the map you want. You must also pass a merge function with this overload.

open as a page

Compare the mutability, null-handling, and concurrency guarantees across the Collectors collection/map factories (toList/toSet/toMap vs their unmodifiable and concurrent variants). When does the choice actually matter?

level: seniorimportance: should knowfreq 40%

basics

~10 s

The plain toList/toSet/toMap give a mutable result of unspecified type and mostly tolerate nulls (except toMap values). The toUnmodifiable* variants give immutable results and reject nulls. toConcurrentMap supports thread-safe parallel collection.

open as a page