Stream API 的懒加载探秘:从直白到深入,谁懒谁勤快?

173 阅读4分钟

Stream API 的懒加载探秘:从直白到深入,谁懒谁勤快?

嘿,大家好!今天咱们再聊聊 Java 的 Stream API 是怎么实现懒加载的。这次我会从一个最简单的例子出发,带你看看它的懒加载是怎么玩的,顺便把哪些方法“懒”、哪些方法“勤快”讲得清清楚楚。咱从直白的方法开始,找出问题,再一步步推到现代高阶方案的逻辑。走起!


先搞明白:懒加载是啥意思?

懒加载,说白了就是“能拖就拖”。在代码里,就是不到必须动手的时候不处理数据。比如我有 100 个数字,想挑出符合条件的再加工,要是全算完再挑,内存和时间不得浪费惨了?懒加载的核心就是“用啥算啥”,按需开动。

Stream API 号称支持懒加载,那它咋实现的呢?咱们从最简单的想法入手。


最直白的办法:啥都立刻干

假设我要从一堆数字里挑出偶数,平方后打印出来。最直白的代码可能是这样:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> result = new ArrayList<>();
for (int num : numbers) {
    if (num % 2 == 0) { // 挑偶数
        result.add(num * num); // 平方
    }
}
result.forEach(System.out::println);

输出:

4
16
36

这代码简单粗暴,循环一遍,挑出 2、4、6,平方成 4、16、36,存起来,打印。没啥问题,对吧?但如果 numbers 有 100 万个元素呢?这法子一次性全干完,中间结果还得占个列表,内存压力蹭蹭涨。要是我只想看前 3 个结果呢?100 万条全算完再挑 3 个,效率也太低了。

这就是“立刻动手”的直白策略的毛病:啥都一股脑干,太积极了。内存占得多,计算浪费大,一点不“懒”。


Stream API 登场:懒在哪儿?

那用 Stream API 改改看:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.stream()
       .filter(num -> num % 2 == 0) // 挑偶数
       .map(num -> num * num)       // 平方
       .forEach(System.out::println);

输出一样:

4
16
36

表面上跟刚才差不多,但这里有个大区别:Stream API 不是立刻全算完的,它是“懒”的。具体咋懒?Stream 的操作分两类:

  • 中间操作:像 filtermap,只是搭个框架,说“要做啥”,但不马上干。
  • 终端操作:像 forEach,喊一声“开始”,才真动手。

上面这代码,filtermap 就像在纸上画了个流程:“先挑偶数,再平方”。但没真算,直到 forEach 说“给我结果”,它才跑起来。这就是懒加载的初步模样:先攒着,等要用再干


懒和勤的清单:哪些方法是啥性格?

读者问得好:除了 mapfilterreduce,还有哪些方法懒?哪些勤快?我这就给你捋清楚。

懒家伙:中间操作

这些方法不会立刻执行,只是搭流水线,等终端操作触发。常见的包括:

  • filter:过滤数据,比如挑偶数,懒着呢。
  • map:转换数据,比如平方,不动手。
  • flatMap:把嵌套结构展平,比如把 List<List> 变 List,也懒。
  • distinct:去重,得看全数据,但还是懒的,只搭计划。
  • sorted:排序,需要全数据,但不立刻排,等触发。
  • peek:偷瞄数据(调试用),不改数据,懒得动。
  • skip:跳过前 n 个,懒。
  • limit:限制个数,懒。
  • takeWhile:满足条件就拿,不满足就停,懒。
  • dropWhile:满足条件就扔,之后全拿,懒。

这些家伙的特点是:只定义规则,不执行。你随便链多少个中间操作,Stream 都不慌,就等着终端操作喊开工。

勤快家伙:终端操作

这些方法一上场,流水线就得跑起来,懒不下了。常见的包括:

  • forEach:遍历输出,立刻干。
  • collect:收集结果,比如转成 List,得全算完。
  • reduce:归约操作,比如求和,得跑完流水线。
  • count:计数,得知道总数,马上算。
  • anyMatch:有没有匹配的,可能早停,但得开始跑。
  • allMatch:全匹配吗,得检查完。
  • noneMatch:全不匹配吗,也得跑。
  • findFirst:找第一个,可能早停,但得动手。
  • findAny:找任意一个,也得干。
  • toArray:转数组,马上执行。

这些家伙一出现,Stream 就从“懒汉”变“劳模”,开始从头到尾跑数据。

小验证:懒得有多彻底?

试试这个:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
Stream<Integer> stream = numbers.stream()
                                .filter(num -> {
                                    System.out.println("过滤: " + num);
                                    return num % 2 == 0;
                                })
                                .map(num -> {
                                    System.out.println("平方: " + num);
                                    return num * num;
                                });
// 啥也没输出,因为没终端操作
stream.limit(2).forEach(System.out::println);

输出:

过滤: 1
过滤: 2
平方: 2
4
过滤: 3
过滤: 4
平方: 4
16

看到没?没加 forEach 前,啥也没干。加了 limit(2)forEach 后,它按需跑,只看了 4 个数,5 和 6 都没碰。这就是懒加载的证据:不到最后不动手


直白懒加载的麻烦:全局操作来了

但懒加载也有尴尬的时候。比如我加个 sorted

numbers.stream()
       .filter(num -> num % 2 == 0)
       .map(num -> num * num)
       .sorted()  // 排序
       .limit(2)
       .forEach(System.out::println);

输出:

4
16

我想拿最小的两个平方数:4 和 16。但 sorted 是啥?它得把所有结果(4、16、36)凑齐,排好序,再给 limit 截前俩。这时候懒加载有点懵——流水线没法一边跑一边挑,得全干完。

这就是直白懒加载的痛点:遇到需要全盘考虑的操作就卡壳