理解 Java Stream API:从实际代码中学习

理解 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
    );

乍一看,这代码挺简洁的,但仔细一想,有几个问题:

  • 为什么第一行是 类名::方法,第二行是 实例::方法
  • flatMapmap 有什么区别?
  • collect 的三个参数都是干嘛的?
  • 第三个参数在串行流中会被调用吗?

带着这些问题,我深入研究了一下,下面分享我的学习心得。


Stream API 基础:链式操作的艺术

Stream API 是 Java 8 引入的函数式编程特性,通过链式调用让数据处理变得优雅。

核心概念

Stream 操作分为两类:

  • 中间操作 :返回新的 Stream,可以链式调用(如 mapfilterflatMap
  • 终端操作 :触发实际计算,返回结果(如 collectforEach

上面的代码就是一个典型的链式操作:

  1. map - 转换元素
  2. filter - 过滤元素
  3. flatMap - 扁平化映射
  4. 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 密集型操作用串行流
  • 需要保持顺序的操作谨慎使用并行流

总结

通过这次学习,我理解了:

  1. 方法引用的规律:根据调用者选择正确的写法
  2. flatMap 的作用:扁平化嵌套的流结构
  3. 自定义收集器:三参数 collect 的用法和必要性
  4. 串行流 vs 并行流:区别和使用场景

Stream API 虽然学习曲线有点陡,但一旦掌握,代码会变得非常优雅。


参考资料

相关推荐
回家路上绕了弯3 小时前
深度解析分布式事务3PC:解决2PC痛点的进阶方案
分布式·后端
狗头大军之江苏分军3 小时前
快手12·22事故原因的合理猜测
前端·后端
仲夏月二十八3 小时前
关于golang中何时使用值对象和指针对象的描述
开发语言·后端·golang
计算机毕设VX:Fegn08953 小时前
计算机毕业设计|基于springboot + vue医院挂号管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
superman超哥4 小时前
仓颉热点代码识别深度解析
开发语言·后端·python·c#·仓颉
ServBay4 小时前
7个Rust写法让代码干净卫生又高效
后端·rust
镜花水月linyi4 小时前
MySQL与Redis缓存一致性方案
redis·后端·mysql
初次攀爬者4 小时前
知识库-向量化功能-读取PDF文件内容的方法
后端