Вопрос 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):

  • Мало коллизий — оверхед незаметен
  • Много коллизий — лучше 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]]