Definition
- toMap(keyMapper, valueMapper, mergeFunction) resolves duplicate keys by combining the existing and new values.
- Double::max returns the larger of two doubles, keeping the higher salary.
- The merge function is called only when a duplicate key is encountered.
- Commutative merge functions like max produce the same result regardless of stream order, making them parallel-safe.
- For exact accounting money, use BigDecimal::max instead of Double::max.
Priya at the National Bank of India indexes salaries by name, but two people are both called Amit. Rather than crash, she agrees on a rule in advance: when a name repeats, keep the larger salary. The merge function is that tie-breaking rule.
Understanding the Concepts
Why does the two-argument toMap fail on duplicate keys?
The two-argument Collectors.toMap throws an IllegalStateException when two stream elements produce the same key because it cannot decide which value to keep. For a name-to-salary map where names can repeat, you must supply a merge function.
package com.bank.hr;
import java.util.*;
import java.util.stream.Collectors;
public class NameToSalaryMap {
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Rajesh", 75000.0),
new Employee("Amit", 68000.0),
new Employee("Amit", 82000.0), // duplicate name
new Employee("Priya", 90000.0)
);
Map<String, Double> nameToSalary = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Employee::getSalaryAsDouble,
Double::max));
System.out.println(nameToSalary);
}
}
{Rajesh=75000.0, Amit=82000.0, Priya=90000.0}
When the second Amit appears, the merge function receives 68000.0 (existing) and 82000.0 (incoming) and returns 82000.0.
How does the merge function work?
The merge function has the signature BinaryOperator: it takes two values of the same type and returns one. It is called only when a duplicate key is detected.
| Merge function | Behaviour | Example | |---|---|---| | Double::max | Keeps the larger value | 68000 vs 82000 = 82000 | | Double::min | Keeps the smaller value | 68000 vs 82000 = 68000 | | Double::sum | Sums both values | 68000 + 82000 = 150000 | | (a, b) -> a | Keeps the first (existing) | 68000 | | (a, b) -> b | Keeps the last (incoming) | 82000 |
// Sum salaries for duplicate names
Map<String, Double> totalPerName = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Employee::getSalaryAsDouble,
Double::sum));
System.out.println(totalPerName);
// {Rajesh=75000.0, Amit=150000.0, Priya=90000.0}
What is the difference between Double::max and BinaryOperator.maxBy?
Double::max is a static method that directly compares two double values. BinaryOperator.maxBy(comparator) wraps a comparator and works with objects, returning the element the comparator considers larger.
import java.util.function.BinaryOperator;
// These are equivalent for Double values:
BinaryOperator<Double> usingMax = Double::max;
BinaryOperator<Double> usingMaxBy = BinaryOperator.maxBy(
Comparator.naturalOrder());
System.out.println(usingMax.apply(68000.0, 82000.0)); // 82000.0
System.out.println(usingMaxBy.apply(68000.0, 82000.0)); // 82000.0
When should you use BigDecimal instead of Double?
Double arithmetic can introduce rounding errors. For display maps, Double is fine. For exact accounting, use BigDecimal.
import java.math.BigDecimal;
Map<String, BigDecimal> exactSalary = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Employee::getSalary, // returns BigDecimal
BigDecimal::max));
What are common mistakes?
Mistake 1: forgetting the merge function when keys can repeat.
// WRONG: throws IllegalStateException on duplicate name
Map<String, Double> crash = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Employee::getSalaryAsDouble));
// CORRECT: supply a merge function
Map<String, Double> safe = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Employee::getSalaryAsDouble,
Double::max));
Mistake 2: using a lambda when a method reference is clearer.
// Verbose
(a, b) -> Math.max(a, b)
// Clean
Double::max
Is Double::max safe for parallel streams?
Yes. Double::max is commutative (max(a, b) == max(b, a)) and associative (max(max(a, b), c) == max(a, max(b, c))), so the result is the same regardless of encounter order. This makes it safe for parallel stream processing.
What should a senior engineer know?
- The merge function is where conflict-resolution policy lives. Choosing max, sum, or keep-latest is a business decision.
- For money, always ask whether the number moves real money. If yes, use BigDecimal. If it is a display figure, Double is acceptable.
- The merge function is not called for unique keys, only for collisions.
- A merge function returning null removes the key from the map (same as Map.merge semantics).
What are common interview follow-up questions?
- "What if you want to keep both salaries?" Change the value mapper to a list and merge by combining lists.
- "How do you sort the resulting map by value?" Stream entrySet, sort by value, collect into LinkedHashMap.
- "What is the time complexity?" O(n) for a single pass, with O(1) per merge call for Double::max.
Visual Overview
Stream<Employee>
[Rajesh:75K, Amit:68K, Amit:82K, Priya:90K]
│
│ toMap(getName, getSalary, Double::max)
▼
Processing:
Rajesh → 75000 (insert, no conflict)
Amit → 68000 (insert, no conflict)
Amit → 82000 (conflict! merge: max(68000, 82000) = 82000)
Priya → 90000 (insert, no conflict)
│
▼
Map<String, Double>
┌──────────────────────────┐
│ Rajesh → 75000.0 │
│ Amit → 82000.0 (max) │
│ Priya → 90000.0 │
└──────────────────────────┘
Key Takeaway
The two-argument toMap throws on duplicate keys because it cannot decide which value to keep. When keys can legitimately repeat, use the three-argument form with a merge function. For a name-to-salary map where you want the higher salary, pass Double::max as the merge function.
When a repeated name appears, the merge function receives the stored salary and the incoming salary and returns the larger one. Because max is commutative, the result is identical regardless of encounter order, making it safe for parallel streams.
The merge function is where conflict-resolution policy lives: max for the highest, sum for totals, keep-latest for the newest. For exact accounting at the National Bank of India, use BigDecimal::max instead of Double::max to preserve every paisa.
Remember one sentence: the three-argument toMap resolves duplicate keys with a merge function, and Double::max keeps the higher value.
---