skip to content

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

level: juniorimportance: must knowfreq 68%

answer

  1. keyMapper → key, valueMapper → value, applied to every element
  2. Function.identity() keeps the whole element as the value
  3. Duplicate key → IllegalStateException (2-arg form)
  4. Return type/mutability unspecified (HashMap today)
  5. null value → NPE because backed by Map.merge

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.

solid answer

~40 s

Collectors.toMap(keyMapper, valueMapper) builds a Map by applying both functions to every stream element: keyMapper produces the entry key, valueMapper produces the entry value. A common idiom maps each object by its id: toMap(Person::id, Function.identity()) gives Map<Id, Person>. The two-argument form has two important constraints: it throws IllegalStateException if two elements map to the same key (no merge defined), and the returned Map type/mutability is unspecified (today a HashMap). The unmodifiable counterpart toUnmodifiableMap has the same duplicate-key behaviour but returns an immutable map and rejects null keys and values. Be careful: a null value from valueMapper causes a NullPointerException with HashMap-backed toMap, since it cannot distinguish 'absent' from 'present-but-null' during the merge.

code

java · 7 lines
java
// One-to-one indexing: id -> Person
Map<Long, Person> byId = people.stream()
    .collect(Collectors.toMap(Person::id, Function.identity()));

// Derive both key and value
Map<String, Integer> lengthByName = names.stream()
    .collect(Collectors.toMap(name -> name, String::length));

go deeper

for a junior

Can write toMap(keyMapper, valueMapper) and explain that one function makes the key and the other the value.

for a middle

Knows the duplicate-key IllegalStateException, uses Function.identity(), and distinguishes toMap from groupingBy.

for a senior

Explains the unspecified return type, the null-value/Map.merge pitfall, and when to escalate to the merge or supplier overloads.

for a principal

Sets conventions for safe map-building (always specify merge for non-unique keys, prefer immutable results), and reasons about null-tolerance and type contracts across an API surface.

## Building a Map from a Stream Sometimes you want to index a collection by some property — e.g. turn a `List<Person>` into a `Map<Long, Person>` keyed by id. `Collectors.toMap` does this. ```java Map<Long, Person> byId = people.stream() .collect(Collectors.toMap(Person::id, Function.identity())); ``` ## The two functions `toMap(keyMapper, valueMapper)` takes **two functions**, each applied to *every* element: - **`keyMapper`** (`Function<T, K>`): given an element, return what its **key** should be. Above, `Person::id` returns the person's id. - **`valueMapper`** (`Function<T, V>`): given an element, return what its **value** should be. `Function.identity()` is a built-in function returning its argument unchanged, so the value is the whole `Person`. For each element the collector computes a key and a value and inserts the pair into the map. ```java // index names by their length Map<String, Integer> lengthByName = names.stream().collect(Collectors.toMap(n -> n, String::length)); ``` ## The duplicate-key trap If **two different elements produce the same key**, the two-argument `toMap` does not know which value should win, so it **throws `IllegalStateException`** (message: "Duplicate key ..."). This is the single most common surprise with `toMap`. Whenever keys are not provably unique (anything but a primary id), you need the three-argument merge overload (a separate topic). ## Return type and mutability The two-argument `toMap` returns a `Map` whose **concrete type and mutability are unspecified** — today it is a mutable `HashMap`, but the contract does not promise that. To force a specific map type (e.g. `TreeMap`, `LinkedHashMap`), use the four-argument overload that takes a map `Supplier`. ## Nulls Because `toMap` is backed by `Map.merge`, a **`null` value** returned by `valueMapper` triggers a `NullPointerException` — `merge` treats `null` as "remove the mapping" and cannot store it. So `toMap` effectively forbids null values. `Collectors.toUnmodifiableMap` additionally rejects **null keys** and returns an immutable map. ## Relation to groupingBy `toMap` is for a **one-to-one** key→value mapping. When many elements share a key and you want them **grouped** (e.g. `Map<Dept, List<Person>>`), that is `Collectors.groupingBy`, a different collector — do not force `toMap` to do grouping with a merge function unless you genuinely want to reduce duplicates to a single value.

  • What does Function.identity() do in toMap?
    It returns its input unchanged, so the value stored is the whole stream element itself — handy for indexing objects by a key while keeping the object as the value.
  • When should you reach for groupingBy instead of toMap?
    When multiple elements share a key and you want to collect all of them (e.g. into a List) rather than reduce them to one value. groupingBy is the natural fit for many-to-one keys.

saying these in an interview costs you the question

  • Forgetting that duplicate keys throw IllegalStateException with the 2-arg form
  • Using toMap to group many values under one key (that is groupingBy)
  • Assuming the result is always a HashMap or always mutable
  • Returning null from valueMapper and expecting it to be stored

context