Вопрос 27 · Раздел 8

Как собрать Stream в Map?

Для сборки в Map используется Collectors.toMap():

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Для сборки в Map используется Collectors.toMap():

toMap() = один элемент на ключ (если ключи дублируются → IllegalStateException). groupingBy() = список элементов на ключ (дубликаты группируются).

Map<Integer, String> idToName = users.stream()
    .collect(Collectors.toMap(
        User::getId,       // ключ
        User::getName      // значение
    ));

Простой пример:

List<User> users = ...;
Map<String, User> nameToUser = users.stream()
    .collect(Collectors.toMap(User::getName, u -> u));

Если у двух элементов одинаковый ключ — получите ошибку IllegalStateException.

🟡 Middle Level

Сигнатуры toMap

Базовая (2 аргумента):

.toMap(keyMapper, valueMapper)
// При коллизии → IllegalStateException

С merge function (3 аргумента):

.toMap(keyMapper, valueMapper, (old, replacement) -> old)
// При коллизии — сохраняет старое значение

// mergeFunction вызывается ТОЛЬКО при дубликате ключа. // Она решает, какое значение оставить: старое, новое, или объединить.

С фабрикой Map (4 аргумента):

.toMap(keyMapper, valueMapper, mergeFn, LinkedHashMap::new)
// Сохраняет порядок вставки

GroupingBy

Для группировки элементов (один ключ — много значений):

// Группировка по городу
Map<City, List<Person>> byCity = persons.stream()
    .collect(groupingBy(Person::getCity));

// Группировка + подсчёт
Map<City, Long> countByCity = persons.stream()
    .collect(groupingBy(Person::getCity, counting()));

Когда НЕ использовать toMap

  1. Ключи могут повторяться — используйте groupingBy(), иначе IllegalStateException
  2. Нужно только количествоgroupingBy(counting()) вместо toMap + size()
  3. Один ключ → много значенийgroupingBy() или toMultimap()

🔴 Senior Level

EnumMap Optimization

Если ключи — Enum, стандартная HashMap неэффективна:

.collect(groupingBy(User::getRole, () -> new EnumMap<>(Role.class), toList()))

EnumMap использует массив → O(1) доступ и минимальная память.

Предотвращение рехеширования

Если знаете размер данных:

.collect(toMap(k, v, merge, () -> new HashMap<>(expectedSize)))

Null Values

Collectors.toMap не позволяет null в значениях, даже если целевая HashMap поддерживает. Это ограничение реализации.

Решение: Используйте forEach или собирайте в Optional.

Identity Function

Используйте Function.identity() вместо x -> x — в некоторых JDK работает эффективнее за счет отсутствия создания лямбда-объекта.

Parallel Streams

groupingByConcurrent для параллельных стримов — использует ConcurrentHashMap, потоки пишут в одну мапу без combiner. Группировка без groupingByConcurrent требует Map.putAll() — может быть медленнее последовательного.

Диагностика

При IllegalStateException в toMap логируйте ключи-дубликаты. Стандартное исключение JDK не всегда информативно.


🎯 Шпаргалка для интервью

Обязательно знать:

  • Collectors.toMap(keyMapper, valueMapper) — базовая сборка, при коллизии бросает IllegalStateException
  • 3-й аргумент toMap — merge function: (old, replacement) -> old (оставить первый)
  • 4-й аргумент — фабрика Map: LinkedHashMap::new сохраняет порядок вставки
  • groupingBy() — когда одному ключу соответствует много значений (Map<K, List>)
  • groupingBy + counting() — для подсчёта элементов по группам
  • toMap не допускает null в значениях — ограничение реализации
  • groupingByConcurrent для параллельных стримов — пишет в ConcurrentHashMap без combiner
  • Для Enum-ключей используйте EnumMap — O(1) доступ, минимальная память

Частые уточняющие вопросы:

  • Что будет при дубликате ключа в toMap? — IllegalStateException. Решение: добавить merge function третьим аргументом.
  • Чем toMap отличается от groupingBy? — toMap: один элемент на ключ; groupingBy: список элементов на ключ (Map<K, List>).
  • Как сохранить порядок вставки? — Четвёртый аргумент: .toMap(key, value, merge, LinkedHashMap::new).
  • Почему toMap не принимает null-значения? — Ограничение внутренней реализации JDK через Map.merge, которая бросает NPE на null.

Красные флаги (НЕ говорить):

  • «toMap автоматически разрешает коллизии» — неверно, без merge function бросит IllegalStateException
  • «groupingBy возвращает Map<K, V>» — неверно, возвращает Map<K, List>
  • «toMap поддерживает null значения» — неверно, бросит NPE
  • «parallelStream с toMap всегда быстрее» — неверно, без groupingByConcurrent может быть медленнее из-за combiner

Связанные темы:

  • [[28. Что делать при коллизиях ключей при сборке в Map]]
  • [[23. Что делают операции distinct(), sorted(), limit(), skip()]]
  • [[29. Как работать с Optional в Stream]]
  • [[21. Что такое lazy evaluation в Stream]]