想真正学懂 Java Stream,绕不过 reduce。
但现实情况是------绝大多数文章只告诉你:
reduce 就是"求和、求最大最小"的。
其实它远远不止这些。
reduce 是整个 Stream 体系里最接近函数式编程灵魂的 API,也是写优雅聚合逻辑的关键。
这篇文章我会带你:
- 从函数定义弄懂为什么 reduce 要三个参数
- 通过图解理解 reduce 的折叠原理
- 串行流 & 并行流真实执行路径
- 各种实战场景(比 "求和" 有用得多)
- 常见错误写法(你一定踩过)
- 最佳实践总结(实际开发最推荐的写法)
保证你看完以后,彻底掌握 reduce。
一、reduce 是什么?一句话理解
reduce = 把一堆元素,通过某种规则,折叠成一个值。
这个"值"可以是:
- 数字
- 字符串
- 对象
- Map
- 统计结构
- 甚至你自定义的任意类型
它的本质是数学中的 Fold(折叠)。
二、reduce 的三个函数定义(最关键的一节)
Java 提供了 3 种 reduce:
① T reduce(T identity, BinaryOperator<T> accumulator)
最常见的版本,带初始值:
css
int sum = list.stream().reduce(0, (a, b) -> a + b);
参数含义:
identity:初始值(种子)accumulator:折叠规则,将"之前的结果 + 当前元素"合并
流程类似:
scss
(((identity ⊕ e1) ⊕ e2) ⊕ e3 ...) ⊕ en
② Optional<T> reduce(BinaryOperator<T> accumulator)
没有 identity,因此 Stream 可能为空 → 返回 Optional。
第一次参与折叠的是前两个元素。
ini
Optional<Integer> max = list.stream().reduce(Integer::max);
③ U reduce(U identity, BiFunction<U, T, U> accumulator, BinaryOperator<U> combiner)
最完整的 reduce,专为 并行流(parallelStream) 设计。
参数含义:
- identity:每个线程分片的初始值
- accumulator:分片内部如何折叠
- combiner:多个分片结果如何合并
python
int totalLen = list.parallelStream().reduce(
0,
(len, s) -> len + s.length(),
Integer::sum
);
这是理解 reduce 真正的关键点。
三、reduce 的底层原理(图解版)
reduce 的底层思想就是 折叠(Fold) 。为了让你真正"看见"它是怎么执行的,这里加入 ASCII 图解,比纯文字清晰 100 倍。
1. 串行 reduce 的完整执行图
假设 Stream:
csharp
[3, 5, 2]
identity:0
accumulator:(a, b) -> a + b
执行流程图:
scss
identity
│
▼
acc(0, 3) = 3
│
▼
acc(3, 5) = 8
│
▼
acc(8, 2) = 10
│
▼
最终结果:10
你可以理解为:
结果不是突然产生的,而是一路"折叠、折叠、再折叠"。
这也是 reduce 名字的含义:归约 / 折叠。
2. 串行流的"单条流水线"结构
Stream 底层会为 reduce 构建出一个处理链:
python
数据源 → map/filter/... → reduce
每个元素会沿着流水线依次通过所有中间操作,最后进入 reduce 折叠。
图示:
python
元素1 → filter → map → reduce
元素2 → filter → map → reduce
元素3 → filter → map → reduce
...
reduce 是整个流水线的最后一步,负责把所有处理过的元素折叠成一个值。
3. 并行 reduce 的执行图(重点 + 高赞内容)
假设输入流:
csharp
[1,2,3,4,5,6]
parallelStream 会把它拆成多个分片(线程并行处理):
分片1:1, 2
分片2:3, 4
分片3:5, 6
每个分片独立执行 accumulator:
scss
分片1:acc(acc(identity,1),2) → r1
分片2:acc(acc(identity,3),4) → r2
分片3:acc(acc(identity,5),6) → r3
得到三个局部结果后,使用 combiner 进行最终归并:
scss
r1
│
▼
combiner(r1, r2) → r12
│
▼
combiner(r12, r3) → finalResult
完整 ASCII 图如下:
markdown
┌────────────┐
元素 1,2 →│ 分片1线程 │→ r1
└────────────┘
┌────────────┐
元素 3,4 →│ 分片2线程 │→ r2
└────────────┘
┌────────────┐
元素 5,6 →│ 分片3线程 │→ r3
└────────────┘
┌────────── combiner ───────────┐
▼ ▼
r1,r2 → combiner(r1,r2) → r12 + r3
▼ ▼
└──────────→ final ─────────────┘
并行 reduce 的关键是:
⚠ identity 在每个线程都会用到一次!
这是很多人踩坑的地方,例如:
scss
parallelStream().reduce(10, Integer::sum);
10 会被加 多次,导致结果错误。
4. reduce 的执行本质图(核心记忆版)
把 reduce 的过程浓缩成一个图:
sql
identity ──► acc ──► acc ──► acc ──► ... ──► final
▲ ▲ ▲
元素1 元素2 元素3
只需要记住:
reduce 从 identity 开始,每遇到一个元素,就调用一次 accumulator,把它"折叠"进去。
四、为什么 reduce 可以完成复杂聚合(原理图解释)
reduce 并不限制返回类型。只要 accumulator 返回同一个类型,你就能折叠:
- 数字
- 字符串
- List
- Map
- 自定义对象(统计对象)
构建统计对象的执行图:
假设有:
ini
users = [u1, u2, u3]
identity = 空统计对象
进行聚合:
scss
stat0 --acc(u1)--> stat1
stat1 --acc(u2)--> stat2
stat2 --acc(u3)--> stat3
图示:
scss
stat(identity)
│
▼
acc(stat,u1)
│
▼
acc(stat,u2)
│
▼
acc(stat,u3)
│
▼
最终统计结构
这就是为什么 reduce 是聚合类操作的核心。
四、并行流(parallelStream)时 reduce 的真实运行方式
假设流是:
csharp
[1,2,3,4,5,6]
parallelStream 会切成多个分片(线程 A/B/C...):
css
线程A:1,2
线程B:3,4
线程C:5,6
每个线程独立执行 accumulator:
scss
A → acc(acc(identity,1),2)
B → acc(acc(identity,3),4)
C → acc(acc(identity,5),6)
得到局部结果:
ra, rb, rc
然后 combiner 负责把它们合并:
ini
final = combiner( combiner(ra, rb), rc )
因此:
⚠ identity 会被每个线程使用一次,而不是只用一次!
例如:
css
parallelStream().reduce(10, (a,b)->a+b)
10 会被多次加进去 → 结果错误。
这是 reduce 最常见的坑。
五、reduce 的最强 8 大实战场景
下面这些才是实际项目中真正有用的 reduce 用法,不是教科书级别的求和实例。
1. 求和(教科书但最常用)
ini
int sum = list.stream().reduce(0, Integer::sum);
2. 求最大值 / 最小值
ini
Optional<Integer> max = list.stream().reduce(Integer::max);
Optional<Integer> min = list.stream().reduce(Integer::min);
3. 统计字符串总长度
ini
int len = list.stream().reduce(
0,
(acc, s) -> acc + s.length(),
Integer::sum
);
4. 构建统计对象(超常用)
比如统计用户年龄总和与数量:
ini
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;
}
);
这是企业项目里非常常见的写法。
5. 用 reduce 把 List 扁平化(不推荐但可做)
scss
List<Integer> merged = lists.stream().reduce(
new ArrayList<>(),
(acc, list) -> { acc.addAll(list); return acc; },
(acc1, acc2) -> { acc1.addAll(acc2); return acc1; }
);
虽然可行,但实际推荐:
scss
lists.stream().flatMap(Collection::stream).toList();
6. 统计出现次数(某字段频率)
python
int count = list.stream()
.filter(s -> s.equals("apple"))
.reduce(0, (acc, s) -> acc + 1, Integer::sum);
不过更推荐 groupingBy。
7. 字符串拼接(可以但不优)
css
String str = list.stream().reduce("", (a, b) -> a + b);
更推荐:
scss
Collectors.joining()
8. 多字段组合运算(如金额 × 汇率)
less
BigDecimal total = list.stream().reduce(
BigDecimal.ZERO,
(acc, o) -> acc.add(o.getAmount().multiply(o.getRate())),
BigDecimal::add
);
企业业务非常常见。
六、reduce 的 4 大误区(你可能全踩过)
❌ 1. 把 reduce 用成"万能循环"
kotlin
.reduce(0, (acc, e) -> {
System.out.println(e); // 副作用,不可取
return acc + e;
})
reduce 应该是"纯函数式",不应做输出、写全局变量等副作用。
❌ 2. 并行流中 identity 使用不当导致结果错误
并行流会让 identity 每个分片都执行一次。
错误示例:
scss
parallelStream().reduce(10, Integer::sum);
❌ 3. 用 reduce 构建 List / Map(低效 + 不安全)
正确方式是:
less
.collect(Collectors.toList())
❌ 4. 忽略 Optional 版本 reduce 的意义
arduino
stream.reduce(Integer::max) // 可能为空
如果你非常确定 stream 非空,可以用有 identity 的重载。
七、什么时候应该果断用 reduce?(最佳实践)
| 场景 | 是否适合 reduce | 推荐程度 |
|---|---|---|
| 求和/最大/最小 | ✔️ 非常适合 | ⭐⭐⭐⭐⭐ |
| 累加数值 | ✔️ 适合 | ⭐⭐⭐⭐⭐ |
| 聚合多个字段成对象 | ✔️ 很适合 | ⭐⭐⭐⭐⭐ |
| 多字段复杂计算 | ✔️ 适合 | ⭐⭐⭐⭐ |
| 拼接字符串 | ❌ 不推荐 | ⭐ |
| 构建 List/Map | ❌ 不推荐 | ⭐ |
结论:
reduce 最适合 "复杂数值聚合" 与 "构建统计对象" 。
这是它的真正价值。
八、总结:reduce 是 Stream 的灵魂
reduce 不是简单的"求和函数",它是:
✔ Stream 中唯一用于"把多个元素归约为一个结果"的 API
✔ 实现自定义聚合逻辑的最强工具
✔ 函数式编程在 Java 里的核心体现
✔ 写优雅、高可读、高抽象代码的关键
如果说 map/filter 是"数据加工工具",那么:
reduce 就是"最终组装器"。
掌握了 reduce,你对 Stream 的理解就上了一个台阶。