理解 Java Stream API:从实际代码中学习
作为一个边缘Java使用者,最近在工作项目中遇到一段 Stream API 的代码,虽然功能实现了,但里面涉及的方法引用、flatMap、自定义收集器等知识点让我有点懵。于是花时间研究了一下,把学习心得整理成这篇博客,希望能帮助到有同样困惑的人。
前言
在代码中看到这样一段代码:
这段代码跟flowable项目有关。
java
JSONObject taskVariableData = list.stream()
.map(HistoricTaskInstance::getExecutionId)
.map(runtimeService::getVariables)
.filter(Objects::nonNull)
.flatMap(map -> map.entrySet().stream())
.collect(
JSONObject::new,
(json, entry) -> json.put(entry.getKey(), entry.getValue()),
JSONObject::putAll
);
乍一看,这代码挺简洁的,但仔细一想,有几个问题:
- 为什么第一行是
类名::方法,第二行是实例::方法? flatMap和map有什么区别?collect的三个参数都是干嘛的?- 第三个参数在串行流中会被调用吗?
带着这些问题,我深入研究了一下,下面分享我的学习心得。
Stream API 基础:链式操作的艺术
Stream API 是 Java 8 引入的函数式编程特性,通过链式调用让数据处理变得优雅。
核心概念
Stream 操作分为两类:
- 中间操作 :返回新的 Stream,可以链式调用(如
map、filter、flatMap) - 终端操作 :触发实际计算,返回结果(如
collect、forEach)
上面的代码就是一个典型的链式操作:
map- 转换元素filter- 过滤元素flatMap- 扁平化映射collect- 收集结果
方法引用:让代码更优雅
方法引用是 Lambda 表达式的简化写法,但什么时候用哪种形式,确实容易搞混。
四种方法引用形式
1. 类名::实例方法(流元素调用自己的方法)
场景: 流中的每个元素调用自己的方法
java
// 字符串转大写
list.stream()
.map(String::toUpperCase)
// 等价于:str -> str.toUpperCase()
// 获取执行ID
list.stream()
.map(HistoricTaskInstance::getExecutionId)
// 等价于:taskInstance -> taskInstance.getExecutionId()
记忆技巧: 流元素调用自己的方法 → 类名::实例方法
2. 实例::实例方法(外部实例处理流元素)
场景: 使用固定的外部实例处理流中的每个元素
java
// 使用固定的 runtimeService 处理每个执行ID
list.stream()
.map(runtimeService::getVariables)
// 等价于:executionId -> runtimeService.getVariables(executionId)
记忆技巧: 外部实例处理流元素 → 实例::实例方法
3. 类名::静态方法
java
list.stream()
.filter(Objects::nonNull)
// 等价于:obj -> Objects.nonNull(obj)
4. 类名::new(构造函数引用)
java
.collect(JSONObject::new, ...)
// 等价于:() -> new JSONObject()
快速记忆表
| 场景 | 写法 | 示例 |
|---|---|---|
| 流元素调用自己的方法 | 类名::实例方法 |
HistoricTaskInstance::getExecutionId |
| 外部实例处理流元素 | 实例::实例方法 |
runtimeService::getVariables |
| 静态方法 | 类名::静态方法 |
Objects::nonNull |
| 构造函数 | 类名::new |
JSONObject::new |
多参数方法的坑
这里有个坑:如果外部实例的方法有多个参数,不能使用方法引用,必须用 Lambda。
java
// ✅ 单参数 - 可以用方法引用
.map(runtimeService::getVariables) // 一个参数:executionId
// ❌ 多参数 - 编译错误!
// .map(runtimeService::getVariable) // 两个参数:executionId, variableName
// ✅ 多参数 - 必须用 Lambda
.map(executionId -> runtimeService.getVariable(executionId, "status"))
解决方案: 如果部分参数固定,可以创建辅助方法:
java
// 创建辅助方法
private Object getStatusVariable(String executionId) {
return runtimeService.getVariable(executionId, "status");
}
// 现在可以用方法引用了
.map(this::getStatusVariable)
flatMap:扁平化映射的神器
flatMap 是我之前一直不太理解的操作,直到看到这段代码才恍然大悟。
map vs flatMap
| 方法 | 输入 | 输出 | 结果结构 |
|---|---|---|---|
map |
Stream | Stream | 一对一,结构不变 |
flatMap |
Stream | Stream | 一对多,扁平化 |
实际例子
java
// 假设经过前面的 map 操作,现在有:
// [Map1{key1=val1, key2=val2}, Map2{key3=val3}, Map3{key4=val4}]
// ❌ 使用 map(错误)
.map(map -> map.entrySet().stream())
// 结果:Stream<Stream<Entry>> - 嵌套的流,无法直接使用
// ✅ 使用 flatMap(正确)
.flatMap(map -> map.entrySet().stream())
// 结果:Stream<Entry> - 扁平化成一个流
// [entry1, entry2, entry3, entry4]
理解要点: flatMap 会把嵌套的流结构"拍平",把 Stream<Stream<T>> 变成 Stream<T>。
执行流程
less
输入:多个 Map 对象
[Map1{key1=val1, key2=val2}, Map2{key3=val3}, Map3{key4=val4}]
flatMap 处理:
.flatMap(map -> map.entrySet().stream())
输出:所有 Map 的 entry 合并成一个流
[entry1, entry2, entry3, entry4]
自定义收集器:collect 的三参数版本
看到 collect 的三个参数,我一开始也懵了。后来查了文档才知道,这是自定义收集器的用法。
三个参数的含义
java
.collect(
JSONObject::new, // 1. 供应者:创建结果容器
(json, entry) -> json.put(entry.getKey(), entry.getValue()), // 2. 累加器:添加元素
JSONObject::putAll // 3. 合并器:合并容器(并行流时用)
);
| 参数 | 作用 | 代码示例 |
|---|---|---|
| Supplier(供应者) | 创建结果容器 | JSONObject::new |
| Accumulator(累加器) | 将元素添加到容器 | (json, entry) -> json.put(...) |
| Combiner(合并器) | 合并两个容器(并行流时用) | JSONObject::putAll |
执行流程
java
// 步骤1:创建容器
JSONObject result = new JSONObject(); // JSONObject::new
// 步骤2:逐个累加(串行流)
result.put("key1", "val1"); // entry1
result.put("key2", "val2"); // entry2
result.put("key3", "val3"); // entry3
// 步骤3:合并(如果是并行流,会有多个JSONObject,需要合并)
// 串行流时这一步不会执行
第三个参数必须写吗?
语法角度: 必须写(三参数版本要求)
功能角度:
- 串行流:不会被调用,但必须提供
- 并行流:会被调用,用于合并多个线程的结果
建议: 即使使用串行流,也保留第三个参数,这样将来改成并行流时,代码可以直接用。
串行流 vs 并行流:什么时候该用哪个?
基本区别
| 类型 | 执行方式 | 创建方式 | 线程使用 |
|---|---|---|---|
| 串行流 | 顺序执行,一个接一个 | list.stream() |
单线程 |
| 并行流 | 并行执行,多个元素同时处理 | list.parallelStream() |
多线程 |
执行示意图
串行流:
元素1 → 处理 → 完成
元素2 → 处理 → 完成
元素3 → 处理 → 完成
并行流:
makefile
线程1: 元素1 → 处理 → 完成
线程2: 元素2 → 处理 → 完成
线程3: 元素3 → 处理 → 完成
并行流中的 Combiner 作用
在并行流中,多个线程会创建多个结果容器,最后需要合并:
java
// 并行流执行过程:
线程1: 创建 JSONObject1 → 添加 entry1, entry2
线程2: 创建 JSONObject2 → 添加 entry3, entry4
线程3: 创建 JSONObject3 → 添加 entry5, entry6
// 最后需要合并(使用 Combiner):
JSONObject1.putAll(JSONObject2) // 合并1和2
JSONObject1.putAll(JSONObject3) // 合并1和3
// 最终得到完整的 JSONObject
什么时候用并行流?
✅ 适合并行流:
- 数据量大(通常 > 10000 条)
- CPU 密集型操作(计算、转换)
- 无状态操作(不依赖其他元素)
- 结果顺序不重要
java
// ✅ 适合并行流
List<Integer> largeList = ...; // 10万条数据
largeList.parallelStream()
.map(x -> x * x) // CPU密集型
.collect(Collectors.toList());
❌ 不适合并行流:
- 数据量小(< 1000 条)
- IO 密集型操作(数据库查询、文件读写)
- 有状态操作(依赖顺序、共享状态)
- 需要保持顺序
java
// ❌ 不适合并行流
list.parallelStream()
.map(id -> database.query(id)) // IO操作,并行可能更慢
.collect(Collectors.toList());
性能对比
java
List<Integer> numbers = IntStream.range(0, 10000000)
.boxed()
.collect(Collectors.toList());
// 串行流:可能耗时 500ms
numbers.stream()
.map(n -> n * 2)
.collect(Collectors.toList());
// 并行流:可能耗时 150ms(在多核CPU上更快)
numbers.parallelStream()
.map(n -> n * 2)
.collect(Collectors.toList());
完整代码解析
让我们回到最初的那段代码,完整分析一下:
java
private JSONObject getTaskVariable(String processInstanceId, String taskId) {
List<HistoricTaskInstance> list = historyService
.createHistoricTaskInstanceQuery()
.processInstanceId(processInstanceId)
.taskId(taskId)
.list();
JSONObject taskVariableData = list.stream()
.map(HistoricTaskInstance::getExecutionId) // 1. 提取执行ID
.map(runtimeService::getVariables) // 2. 获取变量Map
.filter(Objects::nonNull) // 3. 过滤空值
.flatMap(map -> map.entrySet().stream()) // 4. 扁平化entry流
.collect(
JSONObject::new, // 5. 创建容器
(json, entry) -> json.put(entry.getKey(), entry.getValue()), // 6. 累加
JSONObject::putAll // 7. 合并(并行流时用)
);
return taskVariableData;
}
执行流程详解
css
步骤1: list.stream()
输入: [task1, task2, task3] (HistoricTaskInstance对象)
步骤2: .map(HistoricTaskInstance::getExecutionId)
输出: ["exec-123", "exec-456", "exec-789"] (执行ID字符串)
步骤3: .map(runtimeService::getVariables)
输出: [Map1{key1=val1, key2=val2}, Map2{key3=val3}, Map3{key4=val4}]
步骤4: .filter(Objects::nonNull)
过滤: 移除所有null的Map
步骤5: .flatMap(map -> map.entrySet().stream())
扁平化: [entry1, entry2, entry3, entry4] (所有Map的entry合并)
步骤6: .collect(...)
收集: 将所有entry合并到一个JSONObject
结果: JSONObject{key1=val1, key2=val2, key3=val3, key4=val4}
等价的传统写法
如果不用 Stream,代码会是这样:
java
JSONObject taskVariableData = new JSONObject();
for (HistoricTaskInstance task : list) {
String executionId = task.getExecutionId();
Map<String, Object> variables = runtimeService.getVariables(executionId);
if (variables != null) {
for (Map.Entry<String, Object> entry : variables.entrySet()) {
taskVariableData.put(entry.getKey(), entry.getValue());
}
}
}
return taskVariableData;
对比:
- Stream 写法:函数式、简洁、可读性强
- 传统写法:命令式、冗长、但更直观
最佳实践
1. 方法引用选择
- 流元素调用自己的方法 →
类名::实例方法 - 外部实例处理流元素 →
实例::实例方法 - 多参数方法 → 用 Lambda 或创建辅助方法
2. flatMap 使用场景
- 需要将嵌套集合展平时用
flatMap - 一对一的转换用
map - 一对多的转换用
flatMap
3. 自定义收集器
- 需要收集到非标准集合类型时用三参数
collect() - 即使串行流也保留第三个参数,以便支持并行流
- 确保累加器和合并器操作是线程安全的
4. 并行流选择
- 大数据量(> 10000条)且 CPU 密集型操作考虑并行流
- 小数据量或 IO 密集型操作用串行流
- 需要保持顺序的操作谨慎使用并行流
总结
通过这次学习,我理解了:
- 方法引用的规律:根据调用者选择正确的写法
- flatMap 的作用:扁平化嵌套的流结构
- 自定义收集器:三参数 collect 的用法和必要性
- 串行流 vs 并行流:区别和使用场景
Stream API 虽然学习曲线有点陡,但一旦掌握,代码会变得非常优雅。
参考资料
- Java 8 Stream API 官方文档
- Flowable 工作流引擎文档
- 《Java 8 实战》(Java 8 in Action)