Back to Stream: map & Transform
Q23Question 23 of 34

Create a Map<Long, String> of employee id to employee name

Key Concepts
  • Collectors.toMap(keyMapper, valueMapper) builds a Map by applying two functions to each element
  • Employee::getId as the key and Employee::getName as the value creates an id-to-name lookup
  • The two-argument toMap throws IllegalStateException on duplicate keys, which surfaces data integrity issues
  • The three-argument form with a merge function resolves duplicate keys explicitly
  • toMap rejects null values by throwing NullPointerException
  • The default map is HashMap; use the four-argument form with a map supplier for ordering or a specific type

Definition

Collectors.toMap(Employee::getId, Employee::getName) turns a list of employees into an id-to-name lookup map.
  • Collectors.toMap(keyMapper, valueMapper) builds a Map by applying a key function and a value function to each stream element.
  • Employee::getId extracts the key; Employee::getName extracts the value.
  • Duplicate keys cause IllegalStateException with the two-argument form.
  • Null values cause NullPointerException because the internal merge logic uses Map.merge.
  • The default implementation is HashMap; use the four-argument form for a specific type.

Priya at the National Bank of India has a pile of employee files and wants a quick phone-book style index: give an employee id and get back the name. She makes an index card for each employee where the id is the label and the name is the content.

Understanding the Concepts

What does Collectors.toMap do?

Collectors.toMap(keyMapper, valueMapper) iterates the stream and applies two functions to each element: one to produce the map key and one to produce the map value. The result is a Map where each element contributes one entry.

JAVA
package com.bank.hr;

import java.util.*;
import java.util.stream.Collectors;

public class IdToNameMap {
    public static void main(String[] args) {
        List<Employee> employees = List.of(
            new Employee(1001L, "Rajesh", "Loans"),
            new Employee(1002L, "Priya",  "IT"),
            new Employee(1003L, "Amit",   "Finance")
        );

        Map<Long, String> idToName = employees.stream()
            .collect(Collectors.toMap(
                Employee::getId,
                Employee::getName));

        System.out.println(idToName);
        System.out.println("ID 1002: " + idToName.get(1002L));
    }
}
TEXT
{1001=Rajesh, 1002=Priya, 1003=Amit}
ID 1002: Priya

What happens when two elements produce the same key?

The two-argument toMap throws an IllegalStateException because it does not know which value to keep. This is actually a useful safety net: it surfaces a data integrity problem rather than silently discarding a record.

JAVA
List<Employee> withDuplicate = List.of(
    new Employee(1001L, "Rajesh", "Loans"),
    new Employee(1001L, "Raj",    "IT")     // same id!
);

// WRONG: throws IllegalStateException
Map<Long, String> crash = withDuplicate.stream()
    .collect(Collectors.toMap(
        Employee::getId,
        Employee::getName));
// Exception: Duplicate key 1001 (attempted merging Rajesh and Raj)

How do you handle duplicate keys?

Use the three-argument form with a merge function that decides which value to keep.

JAVA
// Keep the first value encountered
Map<Long, String> keepFirst = withDuplicate.stream()
    .collect(Collectors.toMap(
        Employee::getId,
        Employee::getName,
        (existing, incoming) -> existing));

// Keep the last value encountered
Map<Long, String> keepLast = withDuplicate.stream()
    .collect(Collectors.toMap(
        Employee::getId,
        Employee::getName,
        (existing, incoming) -> incoming));

// Concatenate both
Map<Long, String> concat = withDuplicate.stream()
    .collect(Collectors.toMap(
        Employee::getId,
        Employee::getName,
        (a, b) -> a + ", " + b));
System.out.println(concat); // {1001=Rajesh, Raj}

How do you control the map type?

The default is HashMap (unordered). Use the four-argument form to specify a different map implementation.

JAVA
// Sorted by id using TreeMap
Map<Long, String> sorted = employees.stream()
    .collect(Collectors.toMap(
        Employee::getId,
        Employee::getName,
        (a, b) -> a,
        TreeMap::new));

// Insertion-order using LinkedHashMap
Map<Long, String> ordered = employees.stream()
    .collect(Collectors.toMap(
        Employee::getId,
        Employee::getName,
        (a, b) -> a,
        LinkedHashMap::new));

What are common mistakes?

Mistake 1: null values cause NullPointerException.

JAVA
// WRONG: null name throws NPE
List<Employee> withNull = List.of(
    new Employee(1001L, null, "Loans")
);
withNull.stream()
    .collect(Collectors.toMap(
        Employee::getId,
        Employee::getName));  // NullPointerException!

// CORRECT: map null to a sentinel
withNull.stream()
    .collect(Collectors.toMap(
        Employee::getId,
        e -> e.getName() != null ? e.getName() : "UNKNOWN"));

Mistake 2: expecting ordered output from HashMap.

JAVA
// The iteration order of HashMap is unpredictable.
// Use TreeMap for sorted keys or LinkedHashMap for insertion order.

How is toMap different from groupingBy?

| Feature | toMap | groupingBy |
|---|---|---|
| Value per key | One value | Collection (List, Set, etc.) |
| Duplicate keys | Throws or merge | Collects into group |
| Use case | Unique keys lookup | One-to-many grouping |
| Default map | HashMap | HashMap |

Use toMap when each key maps to exactly one value. Use groupingBy when multiple elements share a key.

What should a senior engineer know?

  • toMap internally uses Map.merge(), which is why null values throw NPE.
  • For an id-keyed lookup, uniqueness is the contract. The two-argument form enforces this and is the correct, intention-revealing choice.
  • In parallel streams, toMap uses a concurrent merge strategy. Supply a ConcurrentHashMap supplier for best parallel performance.
  • When building a lookup that is queried frequently, the O(1) HashMap lookup amortises the O(n) collection cost.

What are common interview follow-up questions?

  • "How do you build a Map keyed by name?" Same pattern: toMap(Employee::getName, Function.identity()).
  • "How do you build an unmodifiable map?" Wrap with Collections.unmodifiableMap() or use Collectors.toUnmodifiableMap() (Java 10+).
  • "How do you handle null keys?" toMap does not reject null keys, but HashMap allows only one null key.

Visual Overview

DIAGRAM
  Stream<Employee>
  [1001:Rajesh, 1002:Priya, 1003:Amit]
       │
       │  Collectors.toMap(getId, getName)
       ▼
  For each element:
    1001 → "Rajesh"   (insert)
    1002 → "Priya"    (insert)
    1003 → "Amit"     (insert)
       │
       ▼
  Map<Long, String>
  ┌──────────────────────┐
  │  1001 → "Rajesh"     │
  │  1002 → "Priya"      │
  │  1003 → "Amit"       │
  └──────────────────────┘

Key Takeaway

To build an id-to-name lookup, collect the employee stream with Collectors.toMap, passing Employee::getId as the key mapper and Employee::getName as the value mapper. The result is a HashMap giving constant-time lookups by id.

Because ids are supposed to be unique, each key appears exactly once. If two employees share an id, the two-argument toMap throws an IllegalStateException, which is a feature: it surfaces a data integrity problem instead of silently dropping a record. When duplicates are expected, supply a merge function in the three-argument form.

Be deliberate about nulls and map type: toMap throws NPE on null values, and HashMap has no ordering. Use the four-argument form with a TreeMap or LinkedHashMap supplier when you need sorted keys or insertion order.

Remember one sentence: toMap(keyMapper, valueMapper) builds a one-to-one lookup map, and the two-argument form deliberately throws on duplicate keys to enforce uniqueness.

---

Share
Use ← → arrow keys to navigate