Why does this Collectors.toMap throw IllegalStateException, and how do you fix it?
salaryByDepartment must return the total salary per department. It passes on a fixture where every employee sits in a different department, and dies in production with:
java.lang.IllegalStateException: Duplicate key Sales (attempted merging values 90000 and 75000)
Constraints:
- the result stays a
Map<String, Integer>built by one terminalcollect - a department's value must be the sum of its employees' salaries
record Employee(String name, String department, int salary) {}
Map<String, Integer> salaryByDepartment(List<Employee> staff) {
return staff.stream()
.collect(Collectors.toMap(Employee::department, Employee::salary));
}
Find and fix the bug.
The two-argument toMap(keyMapper, valueMapper) has no rule for two elements landing on the same key, so the second Sales employee makes it throw. Use the three-argument overload and give it a merge function — toMap(Employee::department, Employee::salary, Integer::sum) — which folds colliding values instead of failing. groupingBy(Employee::department, summingInt(...)) is the equivalent.
- ✗Assuming
toMapoverwrites a duplicate key the wayMap.putdoes, instead of throwing - ✗Reaching for
distinct()or a pre-filter rather than telling the collector how to merge colliding values - ✗Forgetting that
toMapalso throwsNullPointerExceptionwhen the value mapper returnsnull
- →When would you prefer
groupingBywith a downstream collector over a three-argumenttoMap? - →How do you make
toMapreturn aTreeMapor aLinkedHashMapinstead of aHashMap?
Walkthrough
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.summingInt;
import static java.util.stream.Collectors.toMap;
Map<String, Integer> salaryByDepartment(List<Employee> staff) {
return staff.stream()
.collect(toMap(Employee::department, Employee::salary, Integer::sum));
}
// the grouping equivalent:
Map<String, Integer> viaGrouping(List<Employee> staff) {
return staff.stream()
.collect(groupingBy(Employee::department, summingInt(Employee::salary)));
}
What happens. The two-argument toMap builds the map with a merge-style insert but owns no merge function. On the second element for the same key it calls its internal throwingMerger() and raises IllegalStateException: Duplicate key Sales (attempted merging values 90000 and 75000). The failure lands on the second employee of a department, never the first — which is why a one-employee-per-department fixture never catches it.
The fix. The three-argument overload toMap(keyMapper, valueMapper, mergeFunction) takes a BinaryOperator invoked exactly on a collision. Integer::sum produces the required per-department total. If the rule were "keep the first one", (a, b) -> a would do.
Nearby trap. toMap throws NullPointerException when the value mapper returns null — unlike HashMap.put, which accepts a null value.