Stream.collect: примеры (JAVA)

Работа метода collect у Stream
Раздел: Потоки данных (Stream API) - терминальные операции
Stream.collect(Collector collector): R

Общее описание

Метод Stream.collect применяется для накопления элементов потока в одну структуру данных или вычисления агрегированного результата. В Java существуют две основные формы этого метода: одна принимает экземпляр интерфейса Collector, другая - три функциональных интерфейса (поставщик контейнера, аккумулятор и комбиниратор). Метод используется при преобразовании стрима в коллекции, карте, строку или при вычислении статистик.

Основные сигнатуры:

R collect(Collector 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

Пример java
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 с подсчётом и сортировкой по частоте

Пример java
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-функцией и созданием неизменяемой карты

Пример java
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

Пример java
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 одновременно

Пример java
// Для 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)

Пример java
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 - постобработка результата

Пример java
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+)

Пример java
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 для корректной и эффективной работы, особенно в параллельных сценариях.

джава Stream.collect function comments

En
Stream.collect Performs a mutable reduction operation on the elements of the stream