Питання 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]]