Що робити при колізіях ключів при збірці в Map?
За замовчуванням toMap кидає помилку:
🟢 Junior Level
Колізія ключів — коли два елементи стрима дають однаковий ключ для Map.
За замовчуванням toMap кидає помилку:
// Якщо два користувачі з однаковим id → IllegalStateException
users.stream().collect(toMap(User::getId, u -> u));
Рішення: Додайте третій параметр — функцію злиття (merge function):
users.stream().collect(toMap(
User::getId,
u -> u,
(existing, replacement) -> existing // залишити перший
));
Варіанти merge function:
(old, newVal) -> old— залишити перше значення(old, newVal) -> newVal— замінити новим(old, newVal) -> old + newVal— об’єднати
// (existing, replacement) -> existing — залишаємо ПЕРШЕ значення // (existing, replacement) -> replacement — залишаємо ОСТАННЄ значення // (existing, replacement) -> existing + replacement — об’єднуємо
🟡 Middle Level
Стратегії розв’язання конфліктів
1. Перезапис (Overwriting):
// Keep First — для дедуплікації
(oldValue, newValue) -> oldValue
// Keep Last — актуальний стан
(oldValue, newValue) -> newValue
2. Агрегація (Collating):
// Конкатенація
.toMap(User::getRole, User::getName, (n1, n2) -> n1 + ", " + n2)
3. Складний вибір (Business Logic):
(existing, replacement) ->
existing.getSalary() > replacement.getSalary() ? existing : replacement
Коли toMap() — поганий вибір?
Якщо одному ключу повинно відповідати кілька значень — використовуйте Collectors.groupingBy():
// Правильно — створить Map<Role, List<User>>
users.stream().collect(groupingBy(User::getRole));
🔴 Senior Level
Merge Function Cost
Мердж-функція викликається в критичній секції збирання. Важкі обчислення загальмують весь стрим.
Parallel Streams
У паралельних стримах колізії обробляються при злитті під-результатів (combiner):
- Мало колізій — overhead непомітний
- Багато колізій — краще
groupingByConcurrent
// У parallelStream combiner викликається для об'єднання результатів від різних воркерів.
// Він повинен бути сумісний з mergeFunction, інакше результат буде неправильним.
Null Values
toMap не переносить null-значення, навіть якщо мердж-функція їх обробляє → NPE всередині Map.merge.
Static Analysis
Інструменти Error Prone (Google) та Sonar позначають toMap без 3-го аргументу як “потенційний баг”. Це стандарт безпечного кодингу.
Діагностика
Для критично важливого коду оберніть toMap у блок, який при помилці логує конфліктуючі об’єкти:
.collect(toMap(
User::getId, u -> u,
(old, newVal) -> {
log.warn("Duplicate key: {}, values: {} and {}", key, old, newVal);
return old;
}
))
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- Колізія ключів = два елементи дають однаковий ключ → без merge function: IllegalStateException
- Стратегії:
(old, new) -> old(залишити перший),(old, new) -> newVal(замінити), об’єднання - Якщо одному ключу потрібно кілька значень — використовуйте
groupingBy(), а не toMap - У паралельних стримах колізії обробляються при злитті під-результатів (combiner)
- Багато колізій у parallelStream — краще
groupingByConcurrentз ConcurrentHashMap - toMap не переносить null-значення → NPE всередині Map.merge, навіть з merge function
- Error Prone та Sonar позначають toMap без 3-го аргументу як «потенційний баг»
Часті уточнюючі запитання:
- Як залишити перше значення при колізії? —
(existing, replacement) -> existing— зберігає перший знайдений елемент. - Коли використовувати groupingBy замість toMap? — Коли одному ключу відповідає кілька значень — groupingBy створить Map<K, List
>. - Чому merge function у parallelStream повинна бути асоціативною? — Тому що combiner може об’єднувати результати в різному порядку — неасоціативна функція дасть недетермінований результат.
- Як залогувати дублікати при колізії? — Обернути merge function у блок з логуванням конфліктуючих об’єктів.
Червоні прапорці (НЕ говорити):
- «toMap без merge function безпечний» — неправда, при дублікатах кине IllegalStateException
- «merge function викликається для кожного елемента» — неправда, тільки при збігу ключів
- «null значення допустимі в toMap» — неправда, Map.merge кине NPE
- «groupingByConcurrent завжди швидший за toMap» — неправда, ефективний тільки при великій кількості колізій
Пов’язані теми:
- [[27. Як зібрати Stream в Map]]
- [[21. Що таке lazy evaluation в Stream]]
- [[22. Коли починається виконання операцій в Stream]]
- [[29. Як працювати з Optional в Stream]]