← Назад к вопросам

Собирал ли 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'а для задачи.

Собирал ли Stream в HashMap | PrepBro