Back to Collectors
Q9Question 9 of 21

Collectors.toMap: keyMapper, valueMapper, merge function, map supplier (LinkedHashMap)

Key Concepts
  • toMap(keyMapper, valueMapper) throws IllegalStateException on duplicate keys, making it a production bug waiting to happen
  • toMap(key, value, mergeFunction) handles duplicates: (existing, incoming) -> existing keeps first, BigDecimal::add sums them
  • toMap(key, value, merge, LinkedHashMap::new) controls the Map implementation for ordering
  • Always provide a merge function in production code to avoid duplicate-key crashes
  • toMap does NOT allow null values: if valueMapper returns null, it throws NullPointerException
  • Function.identity() as the value mapper builds entity lookup maps: toMap(Trade::getId, identity())
  • Default Map is HashMap (random order); use LinkedHashMap for insertion order or TreeMap for sorted keys

Definition

Collectors.toMap collects stream elements into a Map by extracting a key and value from each element, with optional merge function for duplicates and map supplier for ordering.
  • Two-arg form – toMap(keyMapper, valueMapper) assumes unique keys; throws IllegalStateException if two elements produce the same key.
  • Three-arg form – toMap(key, value, mergeFunction) resolves key collisions: (existing, incoming) -> existing keeps first, BigDecimal::add sums them.
  • Four-arg form – toMap(key, value, merge, mapSupplier) controls the Map type: LinkedHashMap::new preserves insertion order, TreeMap::new sorts by key.
  • Null values – toMap does NOT allow null values; a null value from the mapper throws NullPointerException.
  • Lookup maps – toMap(Trade::getId, Function.identity()) builds a Map for O(1) lookups.

You have 10 trades and want a Map of symbol to amount. Two RELIANCE trades? Without a merge function, the system crashes with "Duplicate key RELIANCE!" With BigDecimal::add as merge, the system sums the amounts. With LinkedHashMap::new, the resulting Map preserves the order symbols first appeared.

Understanding the Concepts

What is Collectors.toMap?

Collectors.toMap collects stream elements into a Map by applying a key extractor and a value extractor to each element.

Why does the two-argument form crash on duplicates?

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
import java.util.function.Function;

public class TradeAggregator {

    // WORKS when keys are unique:
    Map<String, BigDecimal> tradeAmounts = trades.stream()
        .collect(Collectors.toMap(Trade::getTradeId, Trade::getAmount));
    // {TRD-001: 245050, TRD-002: 176000}

    // CRASHES when keys duplicate:
    // Two trades have symbol "RELIANCE" → duplicate key!
    Map<String, BigDecimal> bySymbol = trades.stream()
        .collect(Collectors.toMap(Trade::getSymbol, Trade::getAmount));
    // IllegalStateException: Duplicate key RELIANCE
}

How does the merge function resolve duplicates?

JAVA
// Keep FIRST value (existing wins):
Map<String, BigDecimal> first = trades.stream()
    .collect(Collectors.toMap(Trade::getSymbol, Trade::getAmount,
        (existing, incoming) -> existing));

// Keep LAST value (incoming wins):
Map<String, BigDecimal> last = trades.stream()
    .collect(Collectors.toMap(Trade::getSymbol, Trade::getAmount,
        (existing, incoming) -> incoming));

// SUM values (aggregate):
Map<String, BigDecimal> total = trades.stream()
    .collect(Collectors.toMap(Trade::getSymbol, Trade::getAmount,
        BigDecimal::add));
// RELIANCE: 245050 + 123750 = 368800

// MAX value:
Map<String, BigDecimal> max = trades.stream()
    .collect(Collectors.toMap(Trade::getSymbol, Trade::getAmount,
        BigDecimal::max));

How do you control the Map implementation?

JAVA
// LinkedHashMap: preserves insertion order
Map<String, BigDecimal> ordered = trades.stream()
    .collect(Collectors.toMap(Trade::getSymbol, Trade::getAmount,
        BigDecimal::add, LinkedHashMap::new));

// TreeMap: sorts by key alphabetically
Map<String, BigDecimal> sorted = trades.stream()
    .collect(Collectors.toMap(Trade::getSymbol, Trade::getAmount,
        BigDecimal::add, TreeMap::new));

How do you build a lookup map?

JAVA
// Entity ID → Entity
Map<String, Trade> tradeLookup = trades.stream()
    .collect(Collectors.toMap(Trade::getTradeId, Function.identity()));
// Function.identity() = the element itself = t -> t

// With merge (if IDs could duplicate):
Map<String, Trade> safeLookup = trades.stream()
    .collect(Collectors.toMap(Trade::getTradeId, Function.identity(),
        (existing, incoming) -> existing));

What about null values?

JAVA
// WRONG: toMap does NOT allow null values
Map<String, String> broken = trades.stream()
    .collect(Collectors.toMap(Trade::getTradeId, Trade::getRiskNote));
// If getRiskNote() returns null → NullPointerException!

// CORRECT: filter nulls or provide a default
Map<String, String> safe = trades.stream()
    .filter(t -> t.getRiskNote() != null)
    .collect(Collectors.toMap(Trade::getTradeId, Trade::getRiskNote));

What are common mistakes?

JAVA
// WRONG: no merge function in production
Map<String, BigDecimal> risky = trades.stream()
    .collect(Collectors.toMap(Trade::getSymbol, Trade::getAmount));
// Works in testing with unique symbols, crashes in production with duplicates!

// CORRECT: always provide merge function when keys might not be unique
Map<String, BigDecimal> safe = trades.stream()
    .collect(Collectors.toMap(Trade::getSymbol, Trade::getAmount, BigDecimal::add));

What should a senior engineer know?

  • The two-arg toMap is the most common stream-related production bug. Code reviews should flag it.
  • For unmodifiable maps: Collectors.toUnmodifiableMap(key, value, merge) (Java 10+).
  • toMap with merge function and LinkedHashMap::new is the most complete form for production code.

What are common interview follow-up questions?

  • What is the default Map type? (HashMap, with no ordering guarantee.)
  • Can toMap produce a ConcurrentHashMap? (Yes: toMap(key, value, merge, ConcurrentHashMap::new).)
  • What is the merge function signature? (BinaryOperator: takes two values, returns one.)

Visual Overview

DIAGRAM
  toMap(symbol, amount, BigDecimal::add, LinkedHashMap::new)

  Stream:  Trade(RELIANCE, 245050)
           Trade(TCS, 176000)
           Trade(RELIANCE, 123750)

  Processing:
  1. RELIANCE → 245050 (new key, add to map)
  2. TCS → 176000 (new key, add to map)
  3. RELIANCE → merge(245050, 123750) = 368800

  Result (LinkedHashMap, insertion order):
  {RELIANCE: 368800, TCS: 176000}

Key Takeaway

Collectors.toMap has three overloads with increasing control. The two-argument form throws IllegalStateException on duplicate keys, which is the most common stream-related production bug. Always provide a merge function in production code: (a, b) -> a keeps first, (a, b) -> b keeps last, BigDecimal::add sums them.

The four-argument form controls the Map implementation: LinkedHashMap::new preserves insertion order, TreeMap::new sorts by key. The default is HashMap with no ordering guarantee. Two additional pitfalls: toMap does not allow null values (NullPointerException if the value mapper returns null), and Function.identity() is the standard pattern for building entity lookup maps.

In a banking trade aggregation service, toMap(symbol, amount, BigDecimal::add, LinkedHashMap::new) sums amounts per symbol and preserves the order symbols were first traded. The merge function prevents the duplicate-key crash that would occur when two RELIANCE trades are in the stream.

Remember one sentence: always provide a merge function with toMap in production code because the two-argument form crashes on duplicate keys.

---

Share
Use ← → arrow keys to navigate