Почему этот Collectors.toMap бросает IllegalStateException и как это исправить?
salaryByDepartment должен возвращать суммарную зарплату по каждому отделу. На фикстуре, где все сотрудники из разных отделов, он проходит, а в проде падает с:
java.lang.IllegalStateException: Duplicate key Sales (attempted merging values 90000 and 75000)
Ограничения:
- результат остаётся
Map<String, Integer>, собранным одним терминальнымcollect - значение отдела — сумма зарплат его сотрудников
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));
}
Найдите и исправьте ошибку.
Двухаргументный toMap(keyMapper, valueMapper) не знает правила для двух элементов, попавших на один ключ, поэтому второй сотрудник из Sales роняет его. Возьмите трёхаргументную перегрузку и дайте ей функцию слияния — toMap(Employee::department, Employee::salary, Integer::sum) — она сворачивает столкнувшиеся значения вместо падения. Эквивалент — groupingBy(Employee::department, summingInt(...)).
- ✗Считать, что
toMapперезаписывает дублирующийся ключ, какMap.put, а не бросает исключение - ✗Хвататься за
distinct()или предварительный фильтр вместо того, чтобы объяснить коллектору, как сливать столкнувшиеся значения - ✗Забывать, что
toMapтакже бросаетNullPointerException, когда valueMapper возвращаетnull
- →Когда стоит предпочесть
groupingByс downstream-коллектором трёхаргументномуtoMap? - →Как заставить
toMapвернутьTreeMapилиLinkedHashMapвместоHashMap?
Разбор
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));
}
// эквивалент через группировку:
Map<String, Integer> viaGrouping(List<Employee> staff) {
return staff.stream()
.collect(groupingBy(Employee::department, summingInt(Employee::salary)));
}
Что происходит. Двухаргументный toMap строит map через merge-подобную вставку, но функции слияния у него нет. Столкнувшись со вторым элементом на том же ключе, он вызывает свой внутренний throwingMerger() и бросает IllegalStateException: Duplicate key Sales (attempted merging values 90000 and 75000). Падение происходит на втором сотруднике отдела, а не на первом — поэтому фикстура «по одному сотруднику на отдел» ошибку не ловит.
Исправление. Трёхаргументная перегрузка toMap(keyMapper, valueMapper, mergeFunction) принимает BinaryOperator, который вызывается ровно при столкновении. Integer::sum даёт требуемую сумму по отделу. Если бы нужно было «оставить первого», подошёл бы (a, b) -> a.
Ловушка рядом. toMap бросает NullPointerException, если valueMapper вернул null, — в отличие от HashMap.put, который null-значение принимает.