在数据处理中,我们经常需要将一系列元素”浓缩”
为一个单一结果:计算总和、寻找极值、拼接字符串,或是构建复杂聚合对象。这类操作在函数式编程中被称为”归约”(Reduction),而在 Java
Stream 中,reduce() 方法正是这一概念的完美体现。

归约:Stream 的聚合引擎
归约操作的本质是将流中的元素反复组合,最终产生一个单一值。这就像把一堆散落的珠子(流元素)穿成一条完整的项链(结果)。Stream
API 提供了三种 reduce() 变体,满足不同场景的需求。

变体一:带初始值的归约

T reduce(T identity, BinaryOperator<T> accumulator);
identity:归约的初始值,也是流为空时的默认返回值

accumulator:累积函数,定义如何合并两个值

特点:总是返回值,不会返回 Optional

变体二:无初始值的归约

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

没有初始值,可能返回空结果

返回值包装在 Optional 中,强制处理空流情况

流为空时返回 Optional.empty()

变体三:支持类型转换的归约

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

最灵活的版本,支持输入输出类型不同

combiner:专门用于并行流中合并部分结果

适合复杂聚合场景

基础归约:从简单聚合开始
让我们从最常见的场景开始——数值计算:

public class BasicReduction {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

        // 1. 求和(带初始值)
        int sum1 = numbers.stream()
                .reduce(0, (a, b) -> a + b);
        System.out.println("总和1: " + sum1);  // 21
        
        // 2. 求和(无初始值)
        Optional<Integer> sum2 = numbers.stream()
                .reduce((a, b) -> a + b);
        sum2.ifPresent(s -> System.out.println("总和2: " + s));  // 21
        
        // 3. 求积
        int product = numbers.stream()
                .reduce(1, (a, b) -> a * b);
        System.out.println("乘积: " + product);  // 720
        
        // 4. 求最大值
        Optional<Integer> max = numbers.stream()
                .reduce(Integer::max);
        max.ifPresent(m -> System.out.println("最大值: " + m));  // 6
        
        // 5. 字符串拼接
        List<String> words = Arrays.asList("Java", "Stream", "Reduction");
        String sentence = words.stream()
                .reduce("", (a, b) -> a + " " + b)
                .trim();
        System.out.println("拼接结果: " + sentence);  // "Java Stream Reduction"
    }

}

理解初始值(identity)的重要性
初始值不仅是计算的起点,还决定了归约操作的数学属性:

List<Integer> numbers = Arrays.asList(1, 2, 3);

// 正确的初始值:加法的单位元是0
int sumCorrect = numbers.stream().reduce(0, (a, b) -> a + b); // 6

// 错误的初始值:结果偏差
int sumWrong = numbers.stream().reduce(10, (a, b) -> a + b); // 16(多了10)

// 乘法单位元是1
int productCorrect = numbers.stream().reduce(1, (a, b) -> a * b); // 6

// 字符串拼接:空字符串是单位元
String concatCorrect = Arrays.asList("a", "b", "c").stream()
.reduce("", (a, b) -> a + b); // "abc"

单位元法则:对于操作 op,如果存在元素 e 使得 e op x = x op e = x 对所有 x 成立,则 e 称为单位元。在归约中,identity
应该是当前操作的单位元。

复杂归约:超越简单计算
归约的真正威力在于处理复杂对象。假设我们需要分析订单数据:

public class OrderAnalysis {
static class Order {
String category;
double amount;
boolean isInternational;
// ... 构造方法和getter
}

    public static void main(String[] args) {
        List<Order> orders = Arrays.asList(
                new Order("Electronics", 299.99, true),
                new Order("Books", 45.50, false),
                new Order("Electronics", 599.99, false),
                new Order("Clothing", 89.99, true),
                new Order("Books", 25.99, false)
        );
        
        // 复杂归约:统计国际订单总金额
        DoubleSummaryStatistics intlStats = orders.stream()
                .filter(Order::isInternational)
                .mapToDouble(Order::getAmount)
                .summaryStatistics();
        
        System.out.println("国际订单统计:");
        System.out.println("  数量: " + intlStats.getCount());
        System.out.println("  总额: " + intlStats.getSum());
        System.out.println("  平均: " + intlStats.getAverage());
        System.out.println("  最高: " + intlStats.getMax());
        System.out.println("  最低: " + intlStats.getMin());
        
        // 自定义归约:构建复杂报表
        OrderReport report = orders.stream()
                .reduce(new OrderReport(),          // 初始报告
                        (r, o) -> r.accumulate(o), // 累积订单到报告
                        OrderReport::combine);     // 合并报告(用于并行)
        
        report.printSummary();
    }
    
    static class OrderReport {
        private double totalRevenue = 0;
        private Map<String, Double> revenueByCategory = new HashMap<>();
        private int orderCount = 0;
        
        // 累积函数:处理单个订单
        OrderReport accumulate(Order order) {
            this.totalRevenue += order.getAmount();
            this.revenueByCategory.merge(
                    order.getCategory(), 
                    order.getAmount(), 
                    Double::sum
            );
            this.orderCount++;
            return this;
        }
        
        // 合并函数:用于并行流
        OrderReport combine(OrderReport other) {
            this.totalRevenue += other.totalRevenue;
            other.revenueByCategory.forEach(
                    (cat, amt) -> this.revenueByCategory.merge(cat, amt, Double::sum)
            );
            this.orderCount += other.orderCount;
            return this;
        }
        
        void printSummary() {
            System.out.println("\n订单报表:");
            System.out.println("总订单数: " + orderCount);
            System.out.println("总收入: $" + totalRevenue);
            System.out.println("按类别收入:");
            revenueByCategory.forEach((cat, rev) -> 
                    System.out.printf("  %s: $%.2f%n", cat, rev)
            );
        }
    }

}

归约的三大黄金法则
为了保证归约操作的正确性(尤其是在并行流中),必须遵守以下约束:

  1. 无状态性(Stateless)
    累积函数不能依赖外部状态,只能基于输入参数计算。
// 错误示例:依赖外部状态
AtomicInteger counter = new AtomicInteger(0);
int sum = numbers.stream().reduce(0, (a, b) -> {
counter.incrementAndGet(); // 副作用!
return a + b;
});

// 正确:纯函数,只依赖输入
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
  1. 无干预性(Non-interfering)
    不修改数据源。
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));

// 危险:在归约中修改源数据
String result = list.stream().reduce("", (a, b) -> {
list.remove(b); // 错误!并发修改异常风险
return a + b;
});
  1. 结合律(Associativity)
    操作顺序不影响结果:(a op b) op c = a op (b op c)
// 加法满足结合律:(1+2)+3 = 1+(2+3) = 6
int sum = Arrays.asList(1, 2, 3).stream()
.reduce(0, (a, b) -> a + b); // 总是6

// 减法不满足结合律:(1-2)-3 ≠ 1-(2-3)
// 因此减法不适合并行归约
并行归约:第三个参数的作用
当使用并行流时,第三个参数 combiner 变得至关重要:

public class ParallelReduction {
public static void main(String[] args) {
List<Integer> numbers = IntStream.range(1, 1_000_000)
.boxed()
.collect(Collectors.toList());

        long start, end;
        
        // 顺序归约
        start = System.currentTimeMillis();
        int seqSum = numbers.stream()
                .reduce(0, Integer::sum);
        end = System.currentTimeMillis();
        System.out.printf("顺序求和: %d (耗时: %dms)%n", seqSum, end - start);
        
        // 并行归约(正确使用combiner)
        start = System.currentTimeMillis();
        int parSum = numbers.parallelStream()
                .reduce(0, 
                        Integer::sum,          // accumulator
                        Integer::sum);         // combiner(与accumulator相同)
        end = System.currentTimeMillis();
        System.out.printf("并行求和: %d (耗时: %dms)%n", parSum, end - start);
        
        // 复杂并行归约:字符串长度统计
        List<String> words = Arrays.asList("Java", "Stream", "Parallel", "Reduction");
        
        int totalLength = words.parallelStream()
                .reduce(0,                          // identity
                        (sum, word) -> sum + word.length(),  // accumulator
                        Integer::sum);              // combiner
        
        System.out.println("总字符数: " + totalLength);  // 22
    }

}

combiner 与 accumulator 的关系
在并行归约中,流被分成多个子任务,每个子任务独立执行 accumulator,最后 combiner 合并子任务结果:

原始数据: [1, 2, 3, 4, 5, 6]
并行分割: [1, 2, 3] 和 [4, 5, 6]

子任务1: acc(0,1)=1 → acc(1,2)=3 → acc(3,3)=6
子任务2: acc(0,4)=4 → acc(4,5)=9 → acc(9,6)=15

最终合并: combiner(6, 15) = 21

归约 vs 特化归约方法
Stream API 提供了许多特化的归约方法,它们在特定场景下更简洁:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// 等价操作对比
Optional<Integer> max1 = numbers.stream().reduce(Integer::max);
Optional<Integer> max2 = numbers.stream().max(Integer::compare);

long count1 = numbers.stream().reduce(0, (a, b) -> a + 1); // 别扭!
long count2 = numbers.stream().count(); // 清晰!

// 数值流特化方法(更高效)
IntSummaryStatistics stats = numbers.stream()
.mapToInt(Integer::intValue)
.summaryStatistics(); // 一次性获取 count, sum, min, max, average

实战:构建灵活的聚合框架
让我们创建一个可复用的归约工具类:

public class ReductionUtils {
// 通用归约构建器
public static <T, R> Collector<T, ?, R> reducing(
R identity,
Function<T, R> mapper,
BinaryOperator<R> op) {

        return Collector.of(
                () -> new Object[]{identity},
                (acc, element) -> acc[0] = op.apply((R) acc[0], mapper.apply(element)),
                (acc1, acc2) -> {
                    acc1[0] = op.apply((R) acc1[0], (R) acc2[0]);
                    return acc1;
                },
                acc -> (R) acc[0]
        );
    }
    
    // 使用示例
    public static void main(String[] args) {
        List<Product> products = Arrays.asList(
                new Product("Laptop", 999.99),
                new Product("Phone", 699.99),
                new Product("Tablet", 399.99)
        );
        
        // 使用自定义归约计算总价值
        Double totalValue = products.stream()
                .collect(reducing(0.0, Product::getPrice, Double::sum));
        
        System.out.printf("商品总价值: $%.2f%n", totalValue);
        
        // 连接商品名称
        String allNames = products.stream()
                .map(Product::getName)
                .collect(reducing("", name -> name + ", ", String::concat));
        
        System.out.println("所有商品: " + allNames.replaceAll(", $", ""));
    }
    
    static class Product {
        String name;
        double price;
        // ... 构造方法和getter
    }

}

性能考虑与最佳实践
选择合适的方法:对于简单聚合,优先使用特化方法(sum(), max() 等)。

注意装箱开销:对基本类型使用 IntStream, LongStream, DoubleStream。

并行化决策:数据量大(通常 > 10000)且操作成本高时考虑并行。

避免状态依赖:确保累积函数是纯函数。

正确使用 identity:确保它是当前操作的数学单位元。

总结
归约操作是 Stream API 的聚合引擎,将 reduce() 方法从简单的数值计算延伸到复杂的业务聚合。通过理解三种变体的适用场景、掌握并行归约中
combiner 的作用,并遵守三大黄金法则,你就能构建出正确、高效且可并行的聚合逻辑。

记住,归约不仅仅是计算工具,更是思维工具——它教会我们将复杂问题分解为简单的累积步骤,这正是函数式编程的核心魅力所在。在接下来的文章中,我们将探索如何将归约与其他
Stream 操作结合,构建完整的数据处理流水线。