很多人学 Stream,只会:
list.stream().map(...).filter(...).collect(...)
一到 reduce 就开始犯怵:
“感觉很高级,但就是想不起来该怎么用。”
更糟的是,大部分教程只会告诉你:
“reduce 可以用来求和、求最大最小。”
其实,reduce 才是 Stream 里真正有味道的那个 API:
- 它能把一堆元素,折叠成任意你想要的“一个结果”;
- 它是各种统计对象、报表聚合逻辑的核心;
- 它直接体现了 Java 中的函数式编程思想。
这篇文章会从 简单 → 本质 → 图解 → 实战 → 适用场景判断 → 踩坑总结,一步一步带你真正吃透 reduce,看完就能在项目里自信地用起来。
一、reduce 是什么?先用一句人话
先别看函数签名,记住一句话就行:
reduce = 把 Stream 里的很多元素,通过一条规则,折叠成一个结果。
这个“结果”可以是:
- 一个数字(sum、max、min、总长度…)
- 一个字符串(不推荐,用 joining 更好)
- 一个对象(统计对象 Stat、Summary、Dto…)
- BigDecimal、Map、甚至你自定义的任何类型
当你脑子里出现:
“我现在手里有一堆数据,想变成一个『总结结果』”
这个时候,十有八九就是 reduce 的舞台。
二、reduce 有哪几种?用最直白的方式讲清楚
Java 中一共有 3 个重载版本:
1)带初始值的 reduce(最常用)
T reduce(T identity, BinaryOperator<T> accumulator)
典型例子:求和。
int sum = nums.stream().reduce(0, (a, b) -> a + b);
// 等价:nums.stream().reduce(0, Integer::sum);
identity:初始值(种子),比如求和时是 0,求积时是 1accumulator:折叠规则,将“之前的结果 + 当前元素”合成新结果
数学上可以写成:
(((identity ⊕ e1) ⊕ e2) ⊕ e3 ...) ⊕ en
2)不带初始值的 reduce
Optional<T> reduce(BinaryOperator<T> accumulator)
由于没有初始值,如果流是空的,就算不出结果,所以返回 Optional<T>。
第一次折叠会使用前两个元素:
Optional<Integer> max = nums.stream().reduce(Integer::max);
3)并行流用的完整版本
<U> U reduce(U identity,
BiFunction<U, T, U> accumulator,
BinaryOperator<U> combiner)
适用于 parallelStream():
identity:每个分片的初始值accumulator:分片内部怎么折叠combiner:多个分片结果怎么合并
例子:统计字符串总长度:
int totalLen = list.parallelStream().reduce(
0,
(len, s) -> len + s.length(),
Integer::sum
);
这个重载是理解并行 reduce 的关键,我们下面会用图解讲清楚。
三、reduce 在干什么?用图先搞懂本质
先看最常见的串行情况。
假设有:
nums = [3, 5, 2]
identity = 0
accumulator = (a, b) -> a + b
串行 reduce 执行过程:
identity = 0
0 + 3 → 3
3 + 5 → 8
8 + 2 → 10
画成一张图更直观:
identity
│
▼
acc(0, 3) = 3
│
▼
acc(3, 5) = 8
│
▼
acc(8, 2) = 10
│
▼
最终结果:10
本质:从 identity 开始,每读到一个元素,就用 accumulator 折叠一次。
这就是 reduce 名字的含义:归约 / 折叠。
四、并行流里的 reduce:图解你一定要看一眼
并行流(parallelStream())是很多人用 reduce 踩坑的地方。
假设:
nums = [1, 2, 3, 4, 5, 6]
并行流会把它拆成多个分片:
分片1:1, 2
分片2:3, 4
分片3:5, 6
每个分片单独执行 accumulator:
分片1:acc(acc(identity,1),2) → r1
分片2:acc(acc(identity,3),4) → r2
分片3:acc(acc(identity,5),6) → r3
然后用 combiner 合并:
final = combiner( combiner(r1, r2), r3 )
完整 ASCII 图:
┌────────────┐
元素 1,2 →│ 分片1线程 │→ r1
└────────────┘
┌────────────┐
元素 3,4 →│ 分片2线程 │→ r2
└────────────┘
┌────────────┐
元素 5,6 →│ 分片3线程 │→ r3
└────────────┘
┌────────── combiner ───────────┐
▼ ▼
r1,r2 → combiner(r1,r2) → r12 + r3
▼ ▼
└──────────→ final ─────────────┘
注意!!这里有一个非常关键的点:
identity 在每个分片都会用一次,不是只用一次。
所以这个写法在并行流下是危险的:
parallelStream().reduce(10, Integer::sum);
10 会在每个分片都加一次,最终结果会比你预期的大很多。
这也是很多人说“parallelStream 结果不对”的核心原因之一。
五、reduce 的高频实战场景(真正在项目里好用的)
很多文章只给你一个“求和”的例子,这里我们看些更贴近实际开发的场景。
1)求和 / 最大 / 最小(教科书但确实常用)
int sum = nums.stream().reduce(0, Integer::sum);
Optional<Integer> max = nums.stream().reduce(Integer::max);
Optional<Integer> min = nums.stream().reduce(Integer::min);
2)构建统计对象(⭐ 强烈推荐,企业开发高频场景)
例如,我们想统计所有用户的总年龄和人数:
class Stat {
int totalAge;
int count;
}
Stat stat = users.stream().reduce(
new Stat(),
(s, u) -> {
s.totalAge += u.getAge();
s.count++;
return s;
},
(s1, s2) -> {
s1.totalAge += s2.totalAge;
s1.count += s2.count;
return s1;
}
);
- 串行时:依次把每个用户“折叠进”同一个 Stat
- 并行时:每个线程有一个自己的 Stat,最后用 combiner 合并
这一类“把一堆对象聚合成一个汇总对象”的场景,reduce 非常适合。
典型应用:
- 订单统计对象(总金额、总件数、订单数…)
- 用户行为统计对象(总访问次数、总时长…)
- 报表 DTO 聚合
3)属性累加:比如字符串总长度
int totalLen = list.stream().reduce(
0,
(acc, s) -> acc + s.length(),
Integer::sum
);
思路非常简单:
累计值 + 当前元素的某个属性 → 新的累计值。
4)多字段计算:金额 × 汇率
BigDecimal total = orders.stream().reduce(
BigDecimal.ZERO,
(acc, o) -> acc.add(o.getAmount().multiply(o.getRate())),
BigDecimal::add
);
这类“遍历所有订单做一堆运算 → 得到一个总值”的需求,用 reduce 写非常自然。
六、什么时候适合用 reduce?什么时候不适合?(这部分很重要)
很多人只是“会写”,但不知道“该不该写”,这里直接给出一套判断标准。
✅ 非常适合用 reduce 的场景
1)数值类聚合(sum / max / min / 总长度等)
原因:
- 本来就是“多个数 → 一个数”的折叠过程
- reduce 写法最直观、最精简
2)构建统计对象 / 汇总对象(Stat、Summary、DTO)
例如:
- 统计总金额、总数量、总订单数
- 统计用户总访问时长、平均停留时间
这类需求天然符合:
“有一个累计对象,每看到一个元素,就把它的信息折叠进去。”
3)多字段复杂计算,最终只要一个结果值
比如上面的“金额 × 汇率”、“权重评分”、“综合指数”等。
只要结果是一个非集合的值,而不是 List / Map,通常都适合用 reduce。
❌ 不适合用 reduce 的场景
1)要构建的是 List / Set / Map 等集合
错误示例:
List<Integer> list = nums.stream().reduce(
new ArrayList<>(),
(l, n) -> { l.add(n); return l; },
(l1, l2) -> { l1.addAll(l2); return l1; }
);
为什么不推荐?
- 可读性差:别人一看根本猜不到你是在“收集元素”
- 并行流下有线程安全问题
- 性能不如
collect
正确写法应该是:
List<Integer> list = nums.stream().collect(Collectors.toList());
记住:想要集合 → 优先考虑 collect,而不是 reduce。
2)需要副作用(打印日志、写数据库、修改外部变量)
stream.reduce(0, (acc, e) -> {
log.info("{}", e); // 副作用
return acc + e;
});
为什么不推荐?
- reduce 设计成“纯函数式”:输入 → 输出,不改外部状态
- 在并行流里会带来线程安全和顺序问题
副作用类操作建议用:
forEach(...)
3)操作不满足结合律,尤其在 parallelStream 中
例如:
- 字符串拼接(顺序不同结果不同)
- 浮点数计算要求完全一致时
并行流会改变执行顺序,如果操作本身不满足:
(a ⊕ b) ⊕ c == a ⊕ (b ⊕ c)
那么结果可能不稳定。
4)只是想“数一数有多少个元素”
int count = list.stream().reduce(0, (acc,e) -> acc + 1);
功能没问题,但:
- 不直观
- 不如
count()高效
正确写法:
long count = list.stream().count();
七、一张表总结:该不该用 reduce?
| 需求类型 | 是否适合用 reduce | 更推荐的方式 |
|---|---|---|
| 求和 / 最大 / 最小 | ✅ 非常适合 | reduce |
| 统计对象(多字段聚合) | ✅ 非常适合 | reduce |
| 复杂计算得到一个数值 | ✅ 适合 | reduce |
| 想要 List / Set / Map | ❌ 不适合 | collect(toList / toSet / toMap) |
| 想打印 / 写库 / 改状态 | ❌ 不适合 | forEach |
| 想做分组 | ❌ 不适合 | Collectors.groupingBy |
| 想统计数量 | ❌ 不适合 | count |
可以用一句话概括:
当你只需要“一个非集合的结果”时,优先考虑 reduce。
八、最后总结几句“金句”(方便你记忆 & 面试吹水)
map/filter是在“加工数据”;reduce是在“做最后的总结”。- 当需求是“很多元素 → 一个结果”,十有八九可以用 reduce 写得更优雅。
- reduce 真正的价值不在“求和”,而在于“把一堆对象聚合成一个统计对象”。
- parallelStream + reduce 时,一定要注意 identity 和结合律问题。
如果你现在已经看完并理解了上面的内容,那你对 reduce 的掌握程度,已经远超大多数只会“求和”的同学了。