Stream.reduce: примеры (JAVA)
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. Показано, как избежать проблем с мутабельностью в параллельном режиме.
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, но пример показывает возможности трёхаргутной версии):
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) Свертка с изменением типа для вычисления статистики без использования готовых классов:
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) Нестандартное применение: построение дерева из последовательности узлов с гарантией ассоциативности:
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 для корректной ассоциативности при параллельном выполнении.