Питання 28 · Розділ 8

Що робити при колізіях ключів при збірці в Map?

За замовчуванням toMap кидає помилку:

Мовні версії: English Russian Ukrainian

🟢 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]]