Definition
- Two-arg form –
toMap(keyMapper, valueMapper)assumes unique keys; throwsIllegalStateExceptionif two elements produce the same key. - Three-arg form –
toMap(key, value, mergeFunction)resolves key collisions:(existing, incoming) -> existingkeeps first,BigDecimal::addsums them. - Four-arg form –
toMap(key, value, merge, mapSupplier)controls the Map type:LinkedHashMap::newpreserves insertion order,TreeMap::newsorts 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 aMapfor 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?
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?
// 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?
// 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?
// 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?
// 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?
// 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
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.
---