Как собрать Stream в Map
Собрать Stream в Map можно двумя основными коллекторами:
🟢 Junior Level
Краткий ответ для собеседования (30 секунд)
Собрать Stream в Map можно двумя основными коллекторами:
Collectors.toMap()— используется для отображения «один-к-одному» ($1 \to 1$), где каждый элемент стрима проецируется ровно в одну записьMap.Entry<K, V>. Если в стриме встречаются дублирующиеся ключи без указания функции разрешения коллизий (merge function), метод выбрасывает исключениеIllegalStateException.Collectors.groupingBy()— используется для отображения «один-ко-многим» ($1 \to N$), группируя элементы по общему ключу классификатора в структуруMap<K, List<V>>.
Примеры использования на Java 21
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
public class StreamToMapDemo {
public record User(int id, String name, String department) {}
public static void main(String[] args) {
List<User> users = List.of(
new User(1, "Алексей", "IT"),
new User(2, "Мария", "HR"),
new User(3, "Иван", "IT")
);
// 1. Базовая сборка тоMap: ID -> Name
Map<Integer, String> idToName = users.stream()
.collect(Collectors.toMap(User::id, User::name));
System.out.println("ID -> Имя: " + idToName);
// 2. Сборка объекта целиком через Function.identity(): ID -> User
Map<Integer, User> idToUser = users.stream()
.collect(Collectors.toMap(User::id, Function.identity()));
System.out.println("ID -> Объект: " + idToUser.keySet());
// 3. Группировка: Department -> List<User>
Map<String, List<User>> byDept = users.stream()
.collect(Collectors.groupingBy(User::department));
System.out.println("IT отдел: " + byDept.get("IT").size() + " чел.");
}
}
🟡 Middle Level
3 перегрузки Collectors.toMap()
| Перегрузка | Аргументы | Поведение при коллизии ключей | Тип возвращаемой Map |
|---|---|---|---|
| 2 аргумента | keyMapper, valueMapper |
💥 Выбрасывает IllegalStateException |
HashMap (по умолчанию) |
| 3 аргумента | keyMapper, valueMapper, mergeFunction |
✅ Вызывает функцию слияния | HashMap (по умолчанию) |
| 4 аргумента | keyMapper, valueMapper, mergeFunction, mapFactory |
✅ Вызывает функцию слияния | Заданный фабрикой (например, TreeMap::new, LinkedHashMap::new) |
Примеры разрешения коллизий и сохранения порядка:
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class ToMapAdvancedDemo {
public record Book(String isbn, String title, int year) {}
public static void main(String[] args) {
List<Book> books = List.of(
new Book("123", "Java Core v1", 2020),
new Book("123", "Java Core v2", 2023), // Дубликат ISBN!
new Book("456", "Spring Boot", 2022)
);
// 1. С mergeFunction: при дубликате оставляем более свежую книгу (новейший год)
Map<String, Book> latestBooks = books.stream()
.collect(Collectors.toMap(
Book::isbn,
b -> b,
(existing, replacement) -> replacement.year() > existing.year() ? replacement : existing
));
System.out.println("ISBN 123 версия: " + latestBooks.get("123").title()); // Java Core v2
// 2. С фабрикой: сбор в LinkedHashMap для сохранения порядка вставки
Map<String, String> orderedMap = books.stream()
.collect(Collectors.toMap(
Book::isbn,
Book::title,
(oldVal, newVal) -> newVal,
LinkedHashMap::new
));
// 3. Сборка в TreeMap для автоматической сортировки по ключам:
Map<String, String> sortedMap = books.stream()
.collect(Collectors.toMap(
Book::isbn,
Book::title,
(oldVal, newVal) -> newVal,
TreeMap::new
));
}
}
🔴 Senior Level
Ловушка NullPointerException при value == null
Один из самых коварных багов в Java Stream API:
List<User> list = List.of(new User(1, null, "IT"));
// 💥 Выбросит NullPointerException, хотя обычная HashMap разрешает null!
Map<Integer, String> map = list.stream()
.collect(Collectors.toMap(User::id, User::name));
Почему это происходит:
В исходном коде OpenJDK метод Collectors.toMap() реализован через вызов Map.merge(key, value, mergeFunction):
// Исходный код Map.java:
default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
Objects.requireNonNull(value); // <-- ЗДЕСЬ ВЫБРАСЫВАЕТСЯ NPE!
...
}
Метод Map.merge считает значение null индикатором удаления записи из мапы, поэтому прямо запрещает передачу null.
Решение для Senior:
Если значения могут быть null:
// Способ 1: Сборка через mutable collect напрямую в HashMap
Map<Integer, String> safeMap = list.stream()
.collect(HashMap::new, (m, u) -> m.put(u.id(), u.name()), HashMap::putAll);
// Способ 2: Обертывание в Optional
Map<Integer, Optional<String>> optMap = list.stream()
.collect(Collectors.toMap(User::id, u -> Optional.ofNullable(u.name())));
Архитектурные оптимизации: EnumMap и предварительное выделение емкости
- Оптимизация для Enum-ключей:
Если ключом является
Enum, обычнаяHashMapтратит лишнюю память на объектыNodeи вычисление хешей. ИспользованиеEnumMapдает прямой доступ по массиву ординалов ($O(1)$) с минимальным memory footprint:Map<Role, List<User>> usersByRole = users.stream() .collect(Collectors.groupingBy(User::role, () -> new EnumMap<>(Role.class), Collectors.toList())); - Предотвращение рехеширования (Presizing HashMap):
При известном размере стрима $N$:
int capacity = (int) Math.ceil(list.size() / 0.75f) + 1; Map<Integer, User> preSized = list.stream() .collect(Collectors.toMap(User::id, Function.identity(), (a, b) -> a, () -> new HashMap<>(capacity)));
🎯 Шпаргалка для интервью
30-секундный ответ
«Собрать Stream в Map можно через
Collectors.toMap()(отображение 1-в-1) илиCollectors.groupingBy()(отображение 1-ко-многим). БазовыйtoMap(key, value)при дубликате ключа бросаетIllegalStateException, поэтому в реальном коде всегда следует передавать 3-й аргументmergeFunction((v1, v2) -> v2). Для контроля типа мапы (например,LinkedHashMapилиTreeMap) используется 4-я перегрузка сmapFactory. Главная ловушкаtoMap: он используетMap.merge(), который категорически запрещает значенияnullи бросаетNullPointerException.»
Сводная таблица параметров toMap
| Количество параметров | Пример вызова | Назначение |
|---|---|---|
| 2 параметра | .toMap(User::id, User::name) |
Быстрый сбор уникальных ключей; падает при дубликатах |
| 3 параметра | .toMap(User::id, User::name, (o, n) -> n) |
Production-стандарт: безопасное слияние дубликатов |
| 4 параметра | .toMap(k, v, merge, LinkedHashMap::new) |
Сохранение порядка вставки элементов |
| 4 параметра | .toMap(k, v, merge, TreeMap::new) |
Автоматическая сортировка записей по ключам |
Топ-4 каверзных вопроса на интервью
1. Почему Collectors.toMap() выбрасывает NullPointerException, если valueMapper возвращает null, даже если целевая HashMap разрешает null?
- Внутри реализации
Collectors.toMap()аккумуляция элементов происходит через методMap.merge(k, v, mergeFunction). Спецификация методаMap.mergeявно требуетObjects.requireNonNull(value), так как в контрактеMap.mergeпередачаnullозначает удаление ключа из таблицы.
2. Что произойдет при дублировании ключа в Collectors.toMap(keyMapper, valueMapper)?
- Будет выброшено исключение
IllegalStateException: Duplicate key <key> (attempted merging values <v1> and <v2>). Для предотвращения ошибки необходимо использовать перегрузку с тремя аргументами, передавBinaryOperator<U> mergeFunction.
3. В чем разница между Collectors.toMap() и Collectors.toConcurrentMap() в parallelStream()?
toMapв параллельном стриме создает независимые мапы в каждом потоке и затем объединяет их черезMap.putAll()в деревеForkJoinPool.toConcurrentMap()имеет характеристикуCONCURRENT: рантайм создает один единственный экземплярConcurrentHashMap, и все потоки пишут в него напрямую без фазы слияния через combiner.
4. Как сохранить порядок элементов при сборке стрима в Map?
- По умолчанию
Collectors.toMap()использует стандартнуюHashMap, которая не сохраняет порядок обхода. Чтобы сохранить порядок вставки (Encounter Order), нужно использовать 4-параметрическую перегрузку с фабрикой:Collectors.toMap(keyMapper, valueMapper, mergeFunction, LinkedHashMap::new).
Типичные ошибки и Red Flags
- 🚩 Забывание
mergeFunctionпри сборе данных из внешних API или БД (приводит к падению сервиса сIllegalStateException). - 🚩 Возврат
nullиз лямбдыvalueMapperвtoMap()(немедленный NPE). - 🚩 Попытка использовать
toMap()вместоgroupingBy(), когда одному ключу должно соответствовать несколько значений. - 🚩 Использование обычной
HashMapвместоEnumMap, когда ключи являются элементамиenum.