Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Stream.collect в HashMap
Да, я часто использовал Stream API для сборки данных в HashMap используя Collectors. Это очень мощный и элегантный способ трансформации данных в Java.
Базовый пример
List<User> users = Arrays.asList(
new User(1L, "John", "john@example.com"),
new User(2L, "Jane", "jane@example.com"),
new User(3L, "Bob", "bob@example.com")
);
// Собрать Stream в HashMap по id
Map<Long, User> usersById = users.stream()
.collect(Collectors.toMap(
User::getId, // key: id
Function.identity() // value: сам объект User
));
// Результат: {1=User(John), 2=User(Jane), 3=User(Bob)}
Использование с трансформацией значений
// Собрать HashMap <id, email>
Map<Long, String> userEmails = users.stream()
.collect(Collectors.toMap(
User::getId, // key: id
User::getEmail // value: email
));
// Результат: {1=john@example.com, 2=jane@example.com, 3=bob@example.com}
Обработка дублей (merge function)
Если может быть несколько объектов с одинаковым key, нужно указать как их мёржить:
List<Order> orders = Arrays.asList(
new Order(1L, "John", 100),
new Order(2L, "John", 200), // Дубль по пользователю!
new Order(3L, "Jane", 150)
);
// НЕПРАВИЛЬНО: Выбросит exception "Duplicate key"
// Map<String, Order> byUser = orders.stream()
// .collect(Collectors.toMap(Order::getUserName, Function.identity()));
// ПРАВИЛЬНО: Указать merge function
Map<String, Integer> totalByUser = orders.stream()
.collect(Collectors.toMap(
Order::getUserName,
Order::getAmount,
Integer::sum // Если дубль, сложить суммы
));
// Результат: {John=300, Jane=150}
Более сложные примеры
1. HashMap <Category, List of Products>
List<Product> products = Arrays.asList(
new Product("Laptop", "Electronics", 1000),
new Product("Mouse", "Electronics", 50),
new Product("Desk", "Furniture", 300),
new Product("Chair", "Furniture", 200)
);
// Группировать по категориям
Map<String, List<Product>> byCategory = products.stream()
.collect(Collectors.groupingBy(
Product::getCategory
));
// Результат:
// {
// Electronics=[Laptop, Mouse],
// Furniture=[Desk, Chair]
// }
2. HashMap <Category, Total Price>
// Группировать И считать сумму
Map<String, Integer> totalByCategory = products.stream()
.collect(Collectors.groupingBy(
Product::getCategory,
Collectors.summingInt(Product::getPrice)
));
// Результат: {Electronics=1050, Furniture=500}
3. HashMap с HashMap inside (nested)
List<Order> orders = Arrays.asList(
new Order(1L, "John", "Electronics", 100),
new Order(2L, "John", "Furniture", 200),
new Order(3L, "Jane", "Electronics", 150)
);
// Группировать по пользователю, потом по категории
Map<String, Map<String, List<Order>>> byUserAndCategory =
orders.stream()
.collect(Collectors.groupingBy(
Order::getUserName,
Collectors.groupingBy(Order::getCategory)
));
// Результат:
// {
// John: {
// Electronics: [Order 1],
// Furniture: [Order 2]
// },
// Jane: {
// Electronics: [Order 3]
// }
// }
Real-world примеры
1. Кэш пользователей по email
@Service
public class UserService {
public Map<String, User> getUsersByEmail(List<Long> userIds) {
return userRepository.findAllById(userIds)
.stream()
.collect(Collectors.toMap(
User::getEmail,
Function.identity()
));
}
}
// Использование
Map<String, User> cache = userService.getUsersByEmail(Arrays.asList(1L, 2L, 3L));
User user = cache.get("john@example.com");
2. Валидация уникальных значений
public boolean validateEmails(List<String> emails) {
Map<String, Long> emailCounts = emails.stream()
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.counting()
));
// Проверить что нет дублей
return emailCounts.values().stream()
.allMatch(count -> count == 1);
}
3. Статистика по заказам
List<Order> orders = repository.findAll();
Map<String, OrderStats> stats = orders.stream()
.collect(Collectors.groupingBy(
Order::getStatus,
Collectors.collectingAndThen(
Collectors.toList(),
list -> new OrderStats(
list.size(), // count
list.stream().mapToLong(o -> o.getAmount()).sum(), // total
list.stream().mapToLong(o -> o.getAmount()).average().orElse(0) // avg
)
)
));
// Результат:
// {
// PENDING: OrderStats(count=10, total=1500, avg=150),
// COMPLETED: OrderStats(count=50, total=7500, avg=150),
// CANCELLED: OrderStats(count=5, total=300, avg=60)
// }
Partition (разделение на true/false)
List<User> users = repository.findAll();
// Разделить на активных и неактивных
Map<Boolean, List<User>> partitioned = users.stream()
.collect(Collectors.partitioningBy(User::isActive));
List<User> activeUsers = partitioned.get(true);
List<User> inactiveUsers = partitioned.get(false);
Custom collector
Когда стандартных collectors не хватает:
public class CustomCollectors {
// Собрать в HashMap с custom merge logic
public static <T, K, V> Collector<T, ?, Map<K, V>> toMapWithMerge(
Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends V> valueMapper,
BiFunction<? super V, ? super V, ? extends V> mergeFunction) {
return Collectors.toMap(keyMapper, valueMapper, mergeFunction);
}
// Использование
Map<Long, User> merged = users.stream()
.collect(CustomCollectors.toMapWithMerge(
User::getId,
Function.identity(),
(existing, incoming) -> incoming // Перезаписать старое на новое
));
}
Проблемы которые я решал
Проблема 1: Null values в HashMap
// НЕПРАВИЛЬНО: NullPointerException если User.getEmail() return null
Map<String, User> map = users.stream()
.collect(Collectors.toMap(User::getEmail, Function.identity()));
// ПРАВИЛЬНО: Филтровать null значения
Map<String, User> map = users.stream()
.filter(u -> u.getEmail() != null)
.collect(Collectors.toMap(User::getEmail, Function.identity()));
// ИЛИ: Использовать nullsLast для sorting
List<User> sorted = users.stream()
.sorted(Comparator.comparing(
User::getEmail,
Comparator.nullsLast(String::compareTo)
))
.collect(Collectors.toList());
Проблема 2: Concurrent modification при работе с HashMap
// Если нужна thread-safe Map
Map<Long, User> concurrentMap = users.stream()
.collect(Collectors.toMap(
User::getId,
Function.identity(),
(a, b) -> a,
ConcurrentHashMap::new // Использовать ConcurrentHashMap
));
Проблема 3: Ленивое выполнение (Lazy evaluation)
// Stream ленивый! Это не выполнится пока не вызовешь terminal operation
Stream<User> stream = users.stream()
.filter(u -> u.isActive())
.map(User::getName);
// Collect — это terminal operation, который выполнит весь stream
Map<Long, String> result = users.stream()
.filter(u -> u.isActive())
.collect(Collectors.toMap(User::getId, User::getName));
Performance tips
1. Избегай multiple iterations
// ПЛОХО: Два отдельных stream'а
List<String> names = users.stream()
.map(User::getName)
.collect(Collectors.toList());
Map<String, User> byName = users.stream()
.collect(Collectors.toMap(User::getName, Function.identity()));
// ХОРОШО: Один stream с несколькими collect
AbstractMap.SimpleEntry<List<String>, Map<String, User>> result =
new AbstractMap.SimpleEntry<>(
users.stream().map(User::getName).collect(Collectors.toList()),
users.stream().collect(Collectors.toMap(User::getName, Function.identity()))
);
// ИЛИ используй custom collector
2. Используй parallelStream для больших данных
// Для 100k+ items параллелизм может ускорить
Map<Long, User> map = users.parallelStream() // Parallel!
.collect(Collectors.toMap(
User::getId,
Function.identity(),
(a, b) -> a,
ConcurrentHashMap::new // MUST be concurrent!
));
Чек-лист при использовании toMap
- Может ли быть null в key? — Используй filter
- Может ли быть дубль в key? — Указать merge function
- Нужна ли thread-safety? — Использовать ConcurrentHashMap
- Большой объём данных? — Попробовать parallelStream
- Нужно хранить оригинальный объект или часть? — Выбрать value mapper
Заключение
Stream.collect(Collectors.toMap(...)) — это очень мощный и элегантный способ трансформации данных в Java. За 10 лет я использовал это в сотнях мест:
- Создание кэшей
- Группировка данных
- Трансформация коллекций
- Статистика и аналитика
- Валидация данных
Это один из моих фаворитов в Java 8+ Stream API.