← Назад к вопросам
Собирал ли Stream в HashMap
1.0 Junior🔥 81 комментариев
#Stream API и функциональное программирование
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Собирание Stream в HashMap: практические примеры
Да, я регулярно использую Stream API с collectors для преобразования данных в HashMap. Это один из самых полезных паттернов в Java для трансформации коллекций. Давайте разберём от простого к сложному.
1. Базовый пример: Stream → HashMap
List<User> users = List.of(
new User(1L, "John", 25),
new User(2L, "Jane", 30),
new User(3L, "Bob", 25)
);
// Простейший случай: id → User
Map<Long, User> usersById = users.stream()
.collect(Collectors.toMap(
User::getId, // key function
Function.identity() // value function (сам объект User)
));
System.out.println(usersById);
// {1=User(1, John, 25), 2=User(2, Jane, 30), 3=User(3, Bob, 25)}
Результат:
Key → Value
1 → User(1, John, 25)
2 → User(2, Jane, 30)
3 → User(3, Bob, 25)
2. Более сложный пример: трансформация значений
// Собираем в HashMap: name → age
Map<String, Integer> nameToAge = users.stream()
.collect(Collectors.toMap(
User::getName, // key: имя
User::getAge // value: возраст
));
System.out.println(nameToAge);
// {John=25, Jane=30, Bob=25}
3. Обработка дубликатов ключей
Что делать, если несколько объектов имеют одно и то же значение ключа?
List<Order> orders = List.of(
new Order(1L, "user1", 100),
new Order(2L, "user1", 200), // Второй заказ для user1!
new Order(3L, "user2", 150)
);
// ❌ Ошибка: IllegalStateException (дубликат ключа)
Map<String, Order> ordersByUser = orders.stream()
.collect(Collectors.toMap(
Order::getUserId,
Function.identity()
)); // Упадёт! Есть два заказа для user1
// ✅ Решение 1: merge function (последний выигрывает)
Map<String, Order> ordersByUser = orders.stream()
.collect(Collectors.toMap(
Order::getUserId,
Function.identity(),
(existing, replacement) -> replacement // Берём новый
));
// {user1=Order(2, user1, 200), user2=Order(3, user2, 150)}
// ✅ Решение 2: merge function (первый выигрывает)
Map<String, Order> ordersByUser = orders.stream()
.collect(Collectors.toMap(
Order::getUserId,
Function.identity(),
(existing, replacement) -> existing // Берём первый
));
// {user1=Order(1, user1, 100), user2=Order(3, user2, 150)}
4. Группировка (groupingBy) — когда нужно много значений на один ключ
List<User> users = List.of(
new User(1L, "John", 25),
new User(2L, "Jane", 30),
new User(3L, "Bob", 25) // Возраст 25, как John
);
// Группируем пользователей по возрасту
Map<Integer, List<User>> usersByAge = users.stream()
.collect(Collectors.groupingBy(User::getAge));
System.out.println(usersByAge);
// {
// 25: [User(1, John, 25), User(3, Bob, 25)],
// 30: [User(2, Jane, 30)]
// }
Разница между toMap и groupingBy:
// toMap: один user на один возраст (ошибка при дубликате)
Map<Integer, User> userByAge = users.stream()
.collect(Collectors.toMap(User::getAge, Function.identity()));
// Упадёт! У нас два пользователя с возрастом 25
// groupingBy: много users на один возраст
Map<Integer, List<User>> usersByAge = users.stream()
.collect(Collectors.groupingBy(User::getAge));
// Работает! Для каждого возраста список пользователей
5. Более сложная трансформация в HashMap
List<Order> orders = List.of(
new Order(1L, "user1", 100),
new Order(2L, "user1", 200),
new Order(3L, "user2", 150),
new Order(4L, "user2", 75)
);
// Собираем сумму заказов по пользователю
Map<String, Integer> totalByUser = orders.stream()
.collect(Collectors.toMap(
Order::getUserId,
Order::getAmount,
Integer::sum // merge function: складываем суммы
));
System.out.println(totalByUser);
// {user1=300, user2=225}
6. Группировка с трансформацией
List<Student> students = List.of(
new Student(1L, "John", "Math", 85),
new Student(2L, "Jane", "Math", 90),
new Student(3L, "Bob", "Physics", 88),
new Student(4L, "Alice", "Physics", 92)
);
// Группируем по предмету и собираем имена
Map<String, List<String>> studentsBySubject = students.stream()
.collect(Collectors.groupingBy(
Student::getSubject,
Collectors.mapping(Student::getName, Collectors.toList())
));
System.out.println(studentsBySubject);
// {Math: [John, Jane], Physics: [Bob, Alice]}
7. Реальный пример: индексирование данных
// Часто нужно быстро найти элемент по ID
List<Product> products = getProductsFromDatabase();
// ✅ После: HashMap для O(1) поиска
Map<String, Product> productMap = products.stream()
.collect(Collectors.toMap(
Product::getSku, // SKU как ключ
Function.identity(),
(p1, p2) -> p1 // Если дубликаты, берём первый
));
// Теперь быстрый поиск
Product product = productMap.get("PROD-123"); // O(1)
8. Специальный collector: LinkedHashMap (сохранение порядка)
List<User> users = List.of(
new User(3L, "Charlie", 25),
new User(1L, "Alice", 30),
new User(2L, "Bob", 25)
);
// HashMap не гарантирует порядок
Map<Long, User> hashMap = users.stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
// Порядок может быть: {2=Bob, 1=Alice, 3=Charlie}
// LinkedHashMap сохраняет порядок вставки
Map<Long, User> linkedMap = users.stream()
.collect(Collectors.toMap(
User::getId,
Function.identity(),
(u1, u2) -> u1,
LinkedHashMap::new // ← Используем LinkedHashMap
));
// Порядок: {3=Charlie, 1=Alice, 2=Bob} (как в списке)
9. Производительность: когда это медленно
// ❌ Неэффективно: два прохода
Map<String, List<User>> grouped = users.stream()
.collect(Collectors.groupingBy(User::getDepartment));
for (List<User> deptUsers : grouped.values()) {
// Ещё один проход!
deptUsers.stream().forEach(u -> process(u));
}
// ✅ Эффективнее: один проход
users.stream()
.collect(Collectors.groupingBy(
User::getDepartment,
Collectors.toList()
))
.forEach((dept, users) -> users.forEach(u -> process(u)));
10. Nested HashMap: HashMap внутри HashMap
// Данные: класс → предмет → оценки
List<Grade> grades = List.of(
new Grade("10A", "Math", 85),
new Grade("10A", "English", 90),
new Grade("10B", "Math", 88),
new Grade("10B", "English", 92)
);
// Собираем вложенную структуру
Map<String, Map<String, Integer>> gradesByClassSubject = grades.stream()
.collect(Collectors.groupingBy(
Grade::getSchoolClass,
Collectors.toMap(
Grade::getSubject,
Grade::getScore
)
));
System.out.println(gradesByClassSubject);
// {
// 10A: {Math: 85, English: 90},
// 10B: {Math: 88, English: 92}
// }
// Использование
Integer mathGrade = gradesByClassSubject.get("10A").get("Math"); // 85
11. Распространённые ошибки
// ❌ Ошибка 1: забыли про merge function с дубликатами
Map<String, User> map = users.stream()
.collect(Collectors.toMap(User::getName, Function.identity()));
// Если два пользователя с одним именем → IllegalStateException
// ✅ Правильно
Map<String, User> map = users.stream()
.collect(Collectors.toMap(
User::getName,
Function.identity(),
(u1, u2) -> u1 // Указываем стратегию
));
// ❌ Ошибка 2: неправильный collector type
Map<String, Integer> counts = words.stream()
.collect(Collectors.toList()); // ← Это List, не Map!
// ✅ Правильно
Map<String, Integer> counts = words.stream()
.collect(Collectors.toMap(
Function.identity(),
word -> 1,
Integer::sum
)); // Подсчёт вхождений слов
12. Performance tip: выбор Map реализации
// HashMap (обычно лучше)
Map<String, User> map1 = users.stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
// LinkedHashMap (сохраняет порядок, слегка медленнее)
Map<String, User> map2 = users.stream()
.collect(Collectors.toMap(
User::getId,
Function.identity(),
(u1, u2) -> u1,
LinkedHashMap::new
));
// ConcurrentHashMap (для многопоточности)
ConcurrentMap<String, User> map3 = users.stream()
.collect(Collectors.toMap(
User::getId,
Function.identity(),
(u1, u2) -> u1,
ConcurrentHashMap::new
));
Вывод
Собирание Stream в HashMap — это базовый скилл современного Java разработчика:
- toMap() — преобразование 1-к-1
- groupingBy() — группировка 1-ко-многим
- merge function — обработка дубликатов ключей
- nested collectors — сложные трансформации
- правильная реализация Map — производительность
Максимум времени я трачу на обработку дубликатов и выбор правильного collector'а для задачи.