Stream.collect: примеры (JAVA)
Stream.collect(Collector super T, A, R> collector): RОбщее описание
Метод Stream.collect применяется для накопления элементов потока в одну структуру данных или вычисления агрегированного результата. В Java существуют две основные формы этого метода: одна принимает экземпляр интерфейса Collector, другая - три функциональных интерфейса (поставщик контейнера, аккумулятор и комбиниратор). Метод используется при преобразовании стрима в коллекции, карте, строку или при вычислении статистик.
Основные сигнатуры:
R collect(Collector super T, A, R> collector)
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner)
Параметры и возвращаемые значения:
- collector - реализация интерфейса
Collector, инкапсулирующая логику создания промежуточного контейнера (supplier), добавления элемента (accumulator), объединения двух промежуточных результатов (combiner), финального преобразования (finisher) и характеристик (Characteristics). Возвращает результат типаR. - supplier - функция без аргументов, создающая новый результат-накопитель (например,
ArrayList::new). - accumulator - BiConsumer, добавляющий элемент потока в контейнер (например,
List::add). - combiner - BiConsumer, объединяющий два промежуточных результата; обязателен при параллельной обработке. Обычно это метод объединения коллекций, например
List::addAll.
Возвращаемое значение - итоговый объект типа R. При использовании Collector финализатор (finisher) может преобразовать внутреннюю аккумуляцию в окончательный тип.
Важные замечания: коллектора бывают мутабельными и немутабельными; при параллельных стримах должен корректно работать combiner. Некоторые предопределённые коллекторы из Collectors поддерживают оптимизации, например параллельное накопление или создание неизменяемых коллекций.
Короткие примеры использования
Примеры показывают различные варианты вызова collect и результирующее значение.
1) Сбор в список (Collectors.toList)
List<String> names = Stream.of("Anna", "Bob", "Cat")
.collect(Collectors.toList());
System.out.println(names);
[Anna, Bob, Cat]
2) Сбор в множество (убираются дубликаты)
Set<String> set = Stream.of("a", "b", "a")
.collect(Collectors.toSet());
System.out.println(set);
[a, b] // порядок не гарантирован
3) Сбор в карту (toMap) с разрешением коллизий
Map<Integer, String> map = Stream.of("one", "two", "three")
.collect(Collectors.toMap(String::length,
s -> s,
(s1, s2) -> s1 + "," + s2));
System.out.println(map);
{3=one,two, 5=three}
4) Собственный аккумулятор (трехаргументный collect)
List<Integer> squares = Stream.of(1,2,3)
.collect(ArrayList::new,
(list, i) -> list.add(i * i),
ArrayList::addAll);
System.out.println(squares);
[1, 4, 9]
5) Объединение строк (Collectors.joining)
String joined = Stream.of("a","b","c")
.collect(Collectors.joining("-"));
System.out.println(joined);
a-b-c
Похожие возможности в Java
Несколько подходов, близких по назначению:
- forEach - выполняет действие для каждого элемента, но не собирает результат в одну структуру. Подходит, когда нужен побочный эффект.
- reduce - агрегирует элементы в одно значение чисто функционально. Хорош для вычисления суммы, произведения или другой свертки, но менее удобен для построения коллекций.
- toArray - быстрый способ получить массив из стрима; предпочтителен, когда нужен именно массив.
- Коллекторы из Collectors - по сути альтернативы реализации через трехаргументный collect; чаще более выразительны и оптимизированы.
Выбор зависит от цели: для формирования коллекций - collect с Collectors, для простых сверток - reduce, для побочных эффектов - forEach, для массива - toArray.
Аналоги в других языках
Краткое сравнение с другими языками и примеры.
JavaScript (Array.prototype.reduce / lodash)
const arr = [1,2,3];
const sum = arr.reduce((acc,v) => acc + v, 0);
console.log(sum);
6
Python (встроенные списки, itertools, functools.reduce)
nums = [1,2,3]
squares = [x*x for x in nums]
print(squares)
[1, 4, 9]
Отличие: list comprehensions более идиоматичны, операции над итераторами дают ленивость, collect как отдельной функции нет.
PHP (array_map, array_reduce)
$arr = [1,2,3];
$sq = array_map(fn($x) => $x*$x, $arr);
print_r($sq);
Array ( [0] => 1 [1] => 4 [2] => 9 )
C# (LINQ .ToList(), .GroupBy(), .ToDictionary())
var list = new [] {1,2,3}.Select(x => x*x).ToList();
Console.WriteLine(string.Join(",", list));
1,4,9
Отличие: LINQ тесно интегрирован в язык, методы похожи по семантике на Collectors.
SQL
SELECT department, COUNT(*) FROM employees GROUP BY department;
department | count ----------+------ HR | 5 IT | 10
Отличие: SQL агрегирует на уровне СУБД, не совпадает с потоковой моделью Java.
Golang (срезы и циклы)
xs := []int{1,2,3}
sq := make([]int, 0, len(xs))
for _, v := range xs { sq = append(sq, v*v) }
fmt.Println(sq)
[1 4 9]
Lua (таблицы и циклы)
local xs = {1,2,3}
local out = {}
for i,v in ipairs(xs) do out[i] = v*v end
print(table.concat(out, ','))
1,4,9
Kotlin (Sequence и коллекции)
val res = sequenceOf(1,2,3).map { it*it }.toList()
println(res)
[1, 4, 9]
Отличия общего характера: у Java Stream.collect строгая типизация и выделенный интерфейс Collector, в других языках часто используются встроенные методы и comprehension-стили, некоторые предлагают ленивые последовательности по умолчанию.
Типичные ошибки
Распространенные ошибки при использовании collect и их проявления.
1) Игнорирование combiner при параллельном стриме
// Неправильно: аккумулятор не поддерживает объединение
List<String> names = Stream.of("a","b","c")
.parallel()
.collect(ArrayList::new,
(list, s) -> list.add(s.toUpperCase()),
(l1, l2) -> { /* пусто */ });
Результат может быть непредсказуемым: потеря элементов или неконсистентность.
2) Коллизии в Collectors.toMap без merge-функции
Map<Integer,String> m = Stream.of("aa","bb","cc")
.collect(Collectors.toMap(String::length, s -> s));
// Для строк с одинаковой длиной будет IllegalStateException
Exception in thread "main" java.lang.IllegalStateException: Duplicate key
3) Мутация возвращаемой коллекции, ожидаемой неизменяемой
List<String> immutable = Stream.of("x").collect(Collectors.toUnmodifiableList());
immutable.add("y");
Exception in thread "main" java.lang.UnsupportedOperationException
4) Использование не-потокобезопасного аккумулятора в параллельном режиме
StringBuilder sb = Stream.of("a","b")
.parallel()
.collect(StringBuilder::new,
(b,s) -> b.append(s),
StringBuilder::append);
System.out.println(sb);
В большинстве случаев корректно, но если accumulator не атомарен, возможна гонка при ручных реализациях.
Рекомендации: при параллельных стримах использовать корректно определенный combiner или готовые коллекторы; для toMap предусмотреть стратегию объединения при дубликатах; использовать toUnmodifiable* только если требуется неизменяемость.
Изменения и дополнения в последних версиях
Эволюция инструментов для collect в JDK:
- Java 8: введен Stream API и интерфейс
Collectorс основными коллекторными утилитами вCollectors. - Java 9: добавлен
Collectors.flatMappingдля упрощения вложенной обработки при группировке. - Java 10: появились
Collectors.toUnmodifiableList,toUnmodifiableSetиtoUnmodifiableMapдля создания неизменяемых коллекций. - Java 12: введён
Collectors.teeingдля одновременного применения двух коллекторов и объединения их результатов в третью структуру.
Эти изменения расширяют возможности по созданию удобных и безопасных сборок результатов и часто используются для улучшения читаемости и безопасности кода.
Расширенные примеры и редкие сценарии
1) Сложная группировка с downstream collector
Map<Integer, List<String>> byLen = Stream.of("apple","ant","banana","bat")
.collect(Collectors.groupingBy(String::length));
System.out.println(byLen);
{3=[ant, bat], 5=[apple], 6=[banana]}
2) groupingBy с подсчётом и сортировкой по частоте
Map<String, Long> freq = Stream.of("a","b","a","c","b","a")
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
freq.entrySet().stream()
.sorted(Map.Entry<String,Long>::comparingByValue)
.forEach(System.out::println);
b=2 a=3 c=1 // порядок по возрастанию частоты
3) toMap с merge-функцией и созданием неизменяемой карты
Map<Integer, String> m = Stream.of("a","bb","ccc","dd")
.collect(Collectors.toUnmodifiableMap(
String::length,
Function.identity(),
(s1, s2) -> s1 + "," + s2));
System.out.println(m);
{1=a, 2=bb,dd, 3=ccc}
4) Пользовательский Collector через Collector.of
Collector<Integer, int[], Integer> sumCollector = Collector.of(
() -> new int[1],
(a, t) -> a[0] += t,
(a, b) -> { a[0] += b[0]; return a; },
a -> a[0],
Collector.Characteristics.UNORDERED);
int sum = Stream.of(1,2,3).collect(sumCollector);
System.out.println(sum);
6
5) Пример с Collectors.teeing (Java 12+) - получение min и max одновременно
// Для JDK 12+
Optional<IntSummaryStatistics> stats = Stream.of(3,1,4,1,5)
.collect(Collectors.collectingAndThen(
Collectors.toList(),
list -> list.stream().mapToInt(Integer::intValue).summaryStatistics()));
System.out.println(stats);
Optional[IntegerSummaryStatistics{count=5, sum=14, min=1, average=2.800000, max=5}]
6) Параллельный collect с контролем combiner (показывает вызовы combiner)
List<String> result = IntStream.range(0, 1000).boxed()
.parallel()
.map(Object::toString)
.collect(ArrayList::new,
ArrayList::add,
(l1, l2) -> { l1.addAll(l2); });
System.out.println(result.size());
1000
7) collectingAndThen - постобработка результата
List<String> immutable = Stream.of("x","y")
.collect(Collectors.collectingAndThen(Collectors.toList(),
Collections::unmodifiableList));
System.out.println(immutable.getClass().getName());
java.util.Collections$UnmodifiableRandomAccessList
8) Использование flatMapping при группировке (Java 9+)
Map<Character, List<String>> letters = Stream.of("apple","apricot","banana")
.collect(Collectors.groupingBy(s -> s.charAt(0),
Collectors.flatMapping(s -> Arrays.stream(s.split("")), Collectors.toList())));
System.out.println(letters);
{a=[a,p,p,l,e,a,p,r,i,c,o,t], b=[b,a,n,a,n,a]}
Эти примеры демонстрируют гибкость Collectors и необходимость правильного выбора комбинации supplier/accumulator/combiner для корректной и эффективной работы, особенно в параллельных сценариях.