讲讲 Java Stream 的惰性求值(Lazy Evaluation)和及早求值(Eager Evaluation)?
在 Java Stream API 中,惰性求值(Lazy Evaluation)和及早求值(Eager Evaluation)是其核心的设计思想。理解这两个概念,是高效、正确使用 Stream 的关键。
简单来说:
- 惰性求值:光说不做,只构建工作流,不实际执行。
- 及早求值:一锤定音,立即执行整个工作流,并产生结果。
下面我们展开详细讲解。
一、 惰性求值(Lazy Evaluation)—— 中间操作
惰性求值对应的是 Stream 的中间操作(Intermediate Operations)。
1. 特点
- 返回值永远是另一个 Stream(例如
Stream<T>、IntStream等)。 - 不触发实际的计算。当你调用一个中间操作时,Java 只是把这个操作记录在了一个“待办清单”(Pipeline)上,并不会去遍历数据源。
2. 常见中间操作
filter(Predicate):过滤map(Function):转换flatMap(Function):扁平化转换distinct():去重sorted():排序limit(long):限制数量skip(long):跳过
3. 示例代码(感知惰性)
java
List<String> names = Arrays.asList("张三", "李四", "王五");
// 注意:这里我们只定义了 Stream,没有调用终结操作
Stream<String> stream = names.stream()
.filter(name -> {
System.out.println("Filter 执行了: " + name);
return name.startsWith("张");
})
.map(name -> {
System.out.println("Map 执行了: " + name);
return name + "老板";
});
System.out.println("Stream 管道构建完毕,准备出发!");
控制台输出:
plaintext
Stream 管道构建完毕,准备出发!
分析:
你会发现控制台没有打印任何 "Filter 执行了..." 或 "Map 执行了..."。这证明了中间操作是惰性的,它们只是“声明”了步骤,并没有真正执行。
二、 及早求值(Eager Evaluation)—— 终结操作
及早求值对应的是 Stream 的终结操作(Terminal Operations)。
1. 特点
- 返回值不是 Stream(可以是
List、Optional、long、甚至void)。 - 触发实际的计算。一旦调用了终结操作,Java 才会开始遍历数据,并按照之前中间操作定义的“待办清单”,一步步处理数据,最后产出结果。
- Stream 只能被消费一次。一旦终结操作执行完毕,该 Stream 就失效了,不能再次使用。
2. 常见终结操作
collect(Collector):收集成 List/Set/Map 等forEach(Consumer):遍历消费reduce(BinaryOperator):规约累计count():计数findFirst()/findAny():查找anyMatch()/allMatch()/noneMatch():匹配
3. 示例代码(触发执行)
我们在上面的代码末尾加上一个终结操作 collect:
java
List<String> result = names.stream()
.filter(name -> {
System.out.println("Filter 评估: " + name);
return name.startsWith("张");
})
.map(name -> {
System.out.println("Map 转换: " + name);
return name + "老板";
})
.collect(Collectors.toList()); // <--- 终结操作,触发及早求值
System.out.println("最终结果: " + result);
控制台输出:
plaintext
Filter 评估: 张三
Map 转换: 张三
Filter 评估: 李四
Filter 评估: 王五
最终结果: [张老板]
三、 为什么 Java Stream 要设计“惰性求值”?(核心优势)
惰性求值并非多此一举,它带来了极大的性能优化空间。主要体现在以下三点:
1. 减少数据遍历次数(Loop Fusion / 循环合并)
在传统的命令式编程中,如果我们先过滤、再转换,通常需要写两个循环。
而在 Stream 中,Java 编译器会将多个中间操作合并。
从上面的控制台输出可以看出,Stream 处理数据的轨迹是纵向的:
- 对“张三”:先 Filter,通过了,紧接着进行 Map。
- 对“李四”:进行 Filter,没通过,直接放弃,开始下一个。
它只对数据源进行了一次遍历,而不是 Filter 遍历一次,Map 再遍历一次。
2. 短路机制(Short-circuiting)
惰性求值使得“无限流”和“提前结束”成为可能。
java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Integer firstEven = numbers.stream()
.filter(n -> {
System.out.println("Filter: " + n);
return n % 2 == 0;
})
.map(n -> {
System.out.println("Map: " + n);
return n * 2;
})
.findFirst() // 只需要找到第一个即可
.orElse(null);
输出:
plaintext
Filter: 1
Filter: 2
Map: 2
分析:
当找到第一个偶数 2 并完成 Map 后,findFirst() 发现目标已达成,立即终止整个 Stream 管道。后面的 3 到 10 根本不会被遍历和处理。这极大地提高了效率。
3. 支持无限流(Infinite Streams)
因为是惰性的,我们可以创建一个理论上无限大小的数据流,只要我们用 limit 限制终结操作即可:
java
// 产生无限个随机数,但只取前 5 个
Stream.generate(Math::random)
.limit(5)
.forEach(System.out.println);
如果没有惰性求值,第一步 Stream.generate 就会直接导致内存溢出(OOM)。
总结对比
| 特性 | 惰性求值 (Lazy Evaluation) | 及早求值 (Eager Evaluation) |
|---|---|---|
| 对应操作 | 中间操作 (Intermediate Operations) | 终结操作 (Terminal Operations) |
| 返回值 | 必须是另一个 Stream |
非 Stream 类型(如 List, void, boolean) |
| 触发时机 | 仅构建管道,不立即执行 | 立即执行,并关闭 Stream |
| 核心作用 | 规划工作流、支持链式调用 | 获取最终结果 |
| 典型代表 | filter, map, limit, distinct |
collect, forEach, count, reduce |
生活比喻:
- 惰性求值就像你在外卖软件上选菜、加入购物车(
filter只要肉,map只要大份)。这个过程你没有花钱,商家也没有做菜。 - 及早求值就像你最后点击了“提交订单并支付”(
collect)。这一瞬间,商家开始洗菜、切菜、炒菜,最终把外卖送到你手上。