基于本文回答
0
评论

讲讲 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(可以是 ListOptionallong、甚至 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 管道。后面的 310 根本不会被遍历和处理。这极大地提高了效率。

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)。这一瞬间,商家开始洗菜、切菜、炒菜,最终把外卖送到你手上。
右滑查看面试常问