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

Метод reduce для потоков в Java
Раздел: Потоки данных (Stream API) - терминальные операции
Stream.reduce(BinaryOperator accumulator): Optional

Общее описание Stream.reduce

Метод Stream.reduce служит для свёртки элементов потока в одно значение. Используется для вычисления агрегатов и свёртки произвольной бинарной операцией. Существует три основных перегрузки, различающиеся набором аргументов и поведением при пустом потоке.

  • Optional<T> reduce(BinaryOperator<T> accumulator)

    Принимает бинарный оператор, который комбинирует два элемента потока в один. Возвращает Optional<T>, так как для пустого потока результата нет.

  • T reduce(T identity, BinaryOperator<T> accumulator)

    Добавляет начальное значение identity, которое используется как нейтральный элемент. Результат всегда возвращается (никогда не Optional). Для пустого потока возвращается значение identity. Подходит, когда есть естественный нейтральный элемент.

  • <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

    Третья форма позволяет сворачивать элементы типа T в результат типа U. accumulator аккумулирует элемент в промежуточный результат, combiner объединяет промежуточные результаты при параллельном выполнении. Необходима ассоциативность операций и корректное поведение идентичного элемента identity относительно операции.

Ключевые требования и нюансы:

  • Операция свёртки должна быть ассоциативной, иначе результат для параллельных потоков может отличаться.
  • identity должен быть нейтральным: для любого x выражение combiner.apply(identity, x) равно x и combiner.apply(x, identity) равно x.
  • Передача мутабельного состояния в accumulator без синхронизации ведёт к ошибкам в параллельном режиме; для мутабельных контейнеров предпочтительнее использовать collect.
  • При использовании первой перегрузки для пустого потока результатом будет пустой Optional; при второй - identity.
  • Если какие-либо аргументы равны null, будет выброшен NullPointerException согласно контракту.

Возвращаемые значения:

  • Optional<T> для однопараметрной версии.
  • T для версии с identity и бинарным оператором.
  • <U> U для трёхаргументной версии, где тип результата может отличаться от типа элементов.

Короткие примеры использования

Несколько типовых случаев с кодом и результатом.

Сумма чисел с Optional из одной перегрузки:

import java.util.*;
import java.util.stream.*;

public class Ex1 {
    public static void main(String[] args) {
        Optional sum = Stream.of(1, 2, 3, 4).reduce(Integer::sum);
        System.out.println(sum);
    }
}
Optional[10]

Сумма с identity, всегда возвращается значение:

import java.util.stream.*;

public class Ex2 {
    public static void main(String[] args) {
        Integer sum = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);
        System.out.println(sum);
    }
}
10

Поведение на пустом потоке: первая версия возвращает пустой Optional, вторая - identity:

import java.util.*;
import java.util.stream.*;

public class Ex3 {
    public static void main(String[] args) {
        Optional opt = Stream.empty().reduce(Integer::sum);
        Integer val = Stream.empty().reduce(0, Integer::sum);
        System.out.println(opt);
        System.out.println(val);
    }
}
Optional.empty
0

Трёхаргутная форма: свёртка в другой тип (строковое представление суммы):

import java.util.stream.*;
import java.util.*;

public class Ex4 {
    public static void main(String[] args) {
        String result = Stream.of(1, 2, 3)
            .reduce("", (s, n) -> s + n + ",", (a, b) -> a + b);
        System.out.println(result);
    }
}
1,2,3,

Умножение через reduce:

import java.util.stream.*;

public class Ex5 {
    public static void main(String[] args) {
        Integer prod = Stream.of(2, 3, 4).reduce(1, (a, b) -> a * b);
        System.out.println(prod);
    }
}
24

Альтернативные методы в Java

  • Stream.collect: более подходящ для мутабельных аккумуляторов, например для построения списков и карт. Работает с Collector и обеспечивает корректную обработку параллельных потоков.
  • Collectors.reducing: возвращает Collector, который выполняет редукцию; удобен при использовании collect(...).
  • mapToInt/Long/Double и sum: для примитивных сумм проще и эффективнее применять специализированные стримы и методы sum(), average() и т. п.
  • IntStream.reduce и другие примитивные версии: специализированы для примитивов, могут быть быстрее и избегают авто-боксинга.

Выбор между reduce и collect определяется характером аккумулятора: для иммутабельных агрегатов reduce подходит; для накопления в мутабельную структуру предпочтительнее collect.

Аналоги в других языках и особенности

Краткое сопоставление поведения reduce и аналогов в популярных языках.

  • JavaScript: Array.prototype.reduce(acc, fn, init). Поведение похоже на Java с identity. Пример:
const arr = [1,2,3];
const sum = arr.reduce((a,b) => a + b, 0);
console.log(sum);
6
  • Python: functools.reduce(function, iterable, initializer). Если initializer не указан и iterable пуст, выбрасывает TypeError. Часто вместо reduce используют встроенные sum(), any(), all() или генераторы.
from functools import reduce
print(reduce(lambda a,b: a+b, [1,2,3], 0))
6
  • PHP: array_reduce(array, callback, initial). Похож на Java с identity.
$arr = [1,2,3];
echo array_reduce($arr, fn($a,$b) => $a+$b, 0);
6
  • C#: LINQ Aggregate(func) и Aggregate(seed, func, funcCombiner). Полностью аналогично Java по наличию перегрузок и комбинирующему методу для параллельных операций (PLINQ).
using System;
using System.Linq;

Console.WriteLine(new[] {1,2,3}.Aggregate(0, (a,b) => a+b));
6
  • Kotlin: Iterable.reduce и fold. fold похож на Java с identity, reduce без начального значения выбрасывает исключение для пустой коллекции.
val list = listOf(1,2,3)
println(list.fold(0) { acc, v -> acc + v })
6
  • Go: Нет встроенного reduce; используется цикл или утилиты. Отсутствие встроенного обеспечивает явное управление и часто лучшую читаемость.
package main
import "fmt"
func main(){
    arr := []int{1,2,3}
    sum := 0
    for _, v := range arr { sum += v }
    fmt.Println(sum)
}
6
  • SQL: агрегатные функции SUM, COUNT, AVG и т. п. выполняют свёртку на стороне СУБД. Отличие: SQL агрегаты работают с наборами и группировками, не с потоком элементов в памяти.

Отличия от Java: Java требует ассоциативности для корректности в параллельных потоках и предоставляет отдельную трёхаргументную форму для изменения типа результата и корректного комбинирования промежуточных результатов.

Типичные ошибки при использовании

  • Использование неассоциативной операции в параллельном потоке: приводит к непредсказуемым результатам. Пример ниже демонстрирует возможную разницу в выводе для асинхронной среды.
import java.util.stream.*;

public class Err1 {
    public static void main(String[] args) {
        Stream s = Stream.of(1,2,3,4);
        // вычитание не ассоциативна
        int r = s.parallel().reduce(0, (a,b) -> a - b, (x,y) -> x - y);
        System.out.println(r);
    }
}
Результат может отличаться от ожидаемого из-за параллельной структуры, например -10 или другие значения
  • Использование мутабельного аккумулятора без combiner и без синхронизации: состояние будет повреждено в параллельном режиме. Пример и нежелательный результат ниже.
import java.util.*;
import java.util.stream.*;

public class Err2 {
    public static void main(String[] args) {
        List nums = Arrays.asList(1,2,3,4,5);
        // попытка собрать в один список через reduce с мутабельным аккумулятором
        List res = nums.parallelStream().reduce(
            new ArrayList(),
            (list, e) -> { list.add(e); return list; },
            (l1, l2) -> { l1.addAll(l2); return l1; }
        );
        System.out.println(res);
    }
}
Результат может быть правильным, но такой подход менее эффективен и более подвержен ошибкам; предпочтительнее collect
  • Ожидание, что reduce с identity и первого варианта эквивалентны: при наличии identity операция вызывается даже на пустом потоке и возвращается identity, тогда как первая форма вернёт Optional.empty.
  • Передача null в аргументы приводит к NullPointerException.

Изменения и эволюция reduce

Метод reduce появился в Java 8 вместе с Stream API. В последующих версиях Ядро API сохраняло сигнатуры без изменений. Дополнительные возможности для свёртки появились через Collectors.reducing и улучшения производительности Stream API в более новых релизах. Нет значительных изменений в семантике reduce в последних версиях, но были оптимизации и улучшения производительности параллельных потоков в JDK.

Расширенные и редкие варианты использования

Несколько продвинутых сценариев применения с пояснениями.

1) Трёхаргутная форма для безопасной параллельной сборки в StringBuilder. Показано, как избежать проблем с мутабельностью в параллельном режиме.

Пример java
import java.util.stream.*;
import java.util.*;

public class Adv1 {
    public static void main(String[] args) {
        StringBuilder sb = Stream.of("a","b","c")
            .parallel()
            .reduce(new StringBuilder(),
                (builder, s) -> builder.append(s),
                (b1, b2) -> { b1.append(b2); return b1; }
            );
        System.out.println(sb.toString());
    }
}
abc

Пояснение: каждая ветвь получает собственный StringBuilder, затем combiner объединяет результаты. Для оптимальной производительности предпочтительнее collect(Collectors.joining()) при работе со строками.

2) Использование reduce для подсчёта частоты слов с явной сменой типа результата (лучше использовать collect, но пример показывает возможности трёхаргутной версии):

Пример java
import java.util.*;
import java.util.stream.*;

public class Adv2 {
    public static void main(String[] args) {
        Stream words = Stream.of("a","b","a","c","b","a");
        Map freq = words.reduce(
            new HashMap(),
            (map, w) -> { map.put(w, map.getOrDefault(w,0)+1); return map; },
            (m1, m2) -> { m2.forEach((k,v) -> m1.merge(k, v, Integer::sum)); return m1; }
        );
        System.out.println(freq);
    }
}
{a=3, b=2, c=1}

Пояснение: пример рабочий, но collect(Collectors.groupingBy(..., counting())) предпочтительнее по читабельности и производительности.

3) Свертка с изменением типа для вычисления статистики без использования готовых классов:

Пример java
import java.util.stream.*;
import java.util.*;

public class Adv3 {
    static class Stats { double sum; int count; }
    public static void main(String[] args) {
        Stats s = Stream.of(2.0, 3.0, 5.0).reduce(
            new Stats(),
            (st, v) -> { st.sum += v; st.count++; return st; },
            (s1, s2) -> { s1.sum += s2.sum; s1.count += s2.count; return s1; }
        );
        System.out.println("avg=" + (s.sum / s.count));
    }
}
avg=3.3333333333333335

Пояснение: для простоты можно было использовать DoubleStream и average(), но пример демонстрирует, как настраивать промежуточный аккумулятор.

4) Нестандартное применение: построение дерева из последовательности узлов с гарантией ассоциативности:

Пример java
import java.util.stream.*;
import java.util.*;

class Node { String v; Node left, right; Node(String v){this.v=v;} public String toString(){return v;} }

public class Adv4 {
    public static void main(String[] args) {
        Node tree = Stream.of("A","B","C","D").reduce(
            null,
            (acc, v) -> acc == null ? new Node(v) : new Node(acc.v + "," + v),
            (n1, n2) -> n1 == null ? n2 : (n2 == null ? n1 : new Node(n1.v + "+" + n2.v))
        );
        System.out.println(tree.v);
    }
}
A,B+C,D

Пояснение: пример иллюстративный; важно тщательно продумывать combiner для корректной ассоциативности при параллельном выполнении.

джава Stream.reduce function comments

En
Stream.reduce Performs a reduction on the elements of the stream using an associative accumulation function