Коллекторы
Завершающая операция collect собирает элементы потока в коллекцию, строку или одно число. Что именно получится, решает коллектор — объект, который описывает способ сборки. Готовые коллекторы лежат в классе Collectors.
collect и toList
Самый простой коллектор собирает элементы в список:
void main() {
List<String> cities = Stream.of("Рим", "Осло", "Лима")
.collect(Collectors.toList());
IO.println(cities);
}Результат:
[Рим, Осло, Лима]С Java 16 у потока есть и короткий метод toList(). Разница в том, что toList() возвращает неизменяемый список, а Collectors.toList() — обычный ArrayList, в который можно добавлять элементы. В новом коде чаще пишут toList().
Множество и строка
Collectors.toSet() собирает элементы во множество и тем самым убирает повторы. Collectors.joining склеивает строки через разделитель, а ещё может добавить начало и конец:
void main() {
List<String> tags = List.of("java", "код", "java", "урок");
Set<String> unique = tags.stream().collect(Collectors.toSet());
IO.println(unique.size());
String line = tags.stream().distinct().collect(Collectors.joining(", ", "[", "]"));
IO.println(line);
}Результат:
3
[java, код, урок]Порядок элементов в HashSet не гарантирован, поэтому здесь выводится только размер множества.
toMap
Collectors.toMap строит словарь. Ему передают две функции: одна получает из элемента ключ, другая — значение:
record Product(String name, int price) {}
void main() {
List<Product> products = List.of(new Product("Чай", 5), new Product("Кофе", 9));
Map<String, Integer> prices = products.stream()
.collect(Collectors.toMap(Product::name, Product::price));
IO.println(prices.get("Кофе"));
}Результат:
9Подсчёты
Некоторые коллекторы сразу считают итог: counting() — количество, summingInt — сумму, averagingDouble — среднее:
record Order(String customer, double amount) {}
void main() {
List<Order> orders = List.of(
new Order("Анна", 20.0),
new Order("Олег", 35.5),
new Order("Анна", 12.5));
long count = orders.stream().collect(Collectors.counting());
double avg = orders.stream().collect(Collectors.averagingDouble(Order::amount));
IO.println("Заказов: " + count);
IO.println("Средний чек: €" + avg);
}Результат:
Заказов: 3
Средний чек: €22.666666666666668Сами по себе эти коллекторы выглядят длиннее, чем count() или average(). Их настоящая сила раскрывается в группировке — о ней следующий урок.
Неизменяемые коллекции
Если результат не нужно менять, используйте Collectors.toUnmodifiableList(), toUnmodifiableSet() или toUnmodifiableMap(). Попытка изменить такую коллекцию выбросит UnsupportedOperationException — это защищает данные от случайной порчи.
Частая ошибка
toMap падает, если два элемента дают одинаковый ключ:
record Order(String customer, double amount) {}
void main() {
List<Order> orders = List.of(new Order("Анна", 20.0), new Order("Анна", 12.5));
Map<String, Double> byCustomer = orders.stream()
.collect(Collectors.toMap(Order::customer, Order::amount));
}Результат:
Exception in thread "main" java.lang.IllegalStateException: Duplicate key Анна (attempted merging values 20.0 and 12.5)
at Main.main(Main.java:7)Если повторы возможны, передайте третьим аргументом функцию слияния — она решает, что делать с двумя значениями одного ключа. Например, Double::sum сложит суммы заказов: Collectors.toMap(Order::customer, Order::amount, Double::sum).