Як зібрати 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. Базова збірка toMap: 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.