☕ 第七章 集合
📄 一句话概括

Stream 不是集合、也不是 I/O 流——它是 JDK8 的数据流水线:数据源 → 中间加工 → 终结出结果,一行代码顶旧写法一个循环。这节课 = 4.7 的 Lambda 正式转正的主场 + 7.4 forEach 的进阶完整版。

1. 先分清三个"流"

名字 是什么 一句话
I/O 流(InputStream) 读文件/网络的 管字节进出,不是本节主角
集合(List/Set/Map) 装数据的仓库 7.1~7.7 学的存货
Stream 加工数据的传送带 本节主角:流水线干活,仓库不动

关键认知:Stream 不存数据,它只是"把集合里的数据拉上传送带加工"的一条管道。加工完的结果要么打印、要么装回新集合——原集合原封不动。

2. 第一步:开流(三种开法)

// ① 集合开流(最常用)—— Collection 自带 stream() 方法
List<String> list = List.of("甲", "乙", "丙");
list.stream();

// ② 数组开流
int[] arr = {3, 1, 2};
Arrays.stream(arr);

// ③ 零散元素直接开流
Stream.of("甲", "乙", "丙");

3. 三段式套路:中间操作 + 终结操作

段位 方法 干什么 特点
中间 filter(条件) 过滤,留下满足条件的 返回新 Stream,可继续链
中间 map(加工) 每个元素加工变形 同上
中间 sorted() 排序 同上
中间 distinct() 去重 同上
中间 limit(n) 只取前 n 个 同上
终结 forEach(动作) 挨个处理 流水线启动,用完即关
终结 count() 数个数,返回 long 同上
终结 collect(Collectors.toList()) 结果装回 List 同上

规矩一眼看穿:中间操作都在"备菜"(返回新 Stream 才能链式接着写),只有终结操作真正"开火"。

完整示例

import java.util.*;
import java.util.stream.*;

List<Integer> scores = List.of(88, 59, 100, 72, 95, 100);

// 筛出 80 分以上 → 去重 → 排序 → 打印
scores.stream()
      .filter(s -> s >= 80)
      .distinct()
      .sorted()
      .forEach(System.out::println);   // 88 95 100

// 结果装回新集合
List<Integer> top = scores.stream()
      .filter(s -> s >= 80)
      .collect(Collectors.toList());

// map 加工变形:每个分数 +5
scores.stream().map(s -> s + 5).forEach(System.out::println);

n -> n % 2 == 0 这种写法就是 4.7 的 Lambda——filter 的参数是个函数式接口 Predicate("判断合同"),你把判断逻辑用 Lambda 塞进去。方法引用 System.out::println 也是 4.7 埋的那颗种子。

4. 两大机关:惰性求值 + 一次性

机关一:惰性求值——没有终结操作,全白写

scores.stream().filter(s -> s >= 80);   // 编译通过,运行啥也不干!

只写中间操作不写终结操作,流水线一行都不执行——Stream 在等"最终命令"。这和 7.4 的迭代器不同(迭代器 next 一次干一次活),Stream 是"攒招式、等开火"。

机关二:流是一次性的

Stream<Integer> s = scores.stream();
s.forEach(System.out::println);   // ✓ 第一次好使
s.forEach(System.out::println);   // ✗ 炸 IllegalStateException

终结操作跑完流就作废,再用要重新开流(scores.stream() 再调一次)。想反复用结果?collect 装回集合最稳妥。

5. 动手时间(Demo08)

import java.util.*;
import java.util.stream.*;

public class Demo08 {
    public static void main(String[] args) {
        List<String> names = List.of("波仔", "墨池", "AI", "波仔", "Java");

        // 1. 去重打印
        names.stream().distinct().forEach(System.out::println);

        // 2. 筛出长度 > 2 的名字,数个数
        long n = names.stream().filter(s -> s.length() > 2).count();
        System.out.println("长度超过2的: " + n + " 个");

        // 3. map 加工:全部变"同学"称呼,装回新集合
        List<String> titled = names.stream()
                .distinct()
                .map(s -> s + "同学")
                .collect(Collectors.toList());
        System.out.println(titled);          // 原集合 names 不变!

        // 4. 机关演示:只有中间操作 = 什么都不会发生
        names.stream().filter(s -> s.length() > 2);   // 控制台安静如鸡

        // 5. 流一次性验证
        Stream<String> st = names.stream();
        st.forEach(System.out::println);
        // st.count();   // 放开注释就炸 IllegalStateException
    }
}

6. 30 秒实验

实验 1:只写 filter 不写终结操作——控制台安静如鸡。惰性求值亲眼验。

实验 2:同一流对象 forEach 两次——第二次炸 IllegalStateException: stream has already been operated upon or closed。

实验 3:流加工前后分别打印原集合——原集合纹丝不动。传送带不还货给仓库。

实验 4:filter(s -> s.length() > 2).count() 之前故意把 filter 换成 map(s -> s + "!")——count 数字变了。中间操作链每环都在真实加工。

实验 5:List.of(...) 造的集合上 .add(...)——炸 UnsupportedOperationException(7.7 不可变那派的又一员,顺带复习)。

7. 常见误区

  1. Stream 是 I/O 流——不是。I/O 流管文件网络字节,Stream 管集合数据加工,只是撞名。
  2. 以为流会改原集合——绝不改。要结果就 collect 装回新集合,或看 forEach 打印。
  3. 流可以反复用——一次性消耗品,终结操作后作废,重开一条即可。
  4. 在 forEach 里改外部变量当结果用——能跑但不地道(并发下还会出事),正经收集用 collect。
  5. 中间操作会立刻执行——惰性求值,攒着呢;没终结操作一行不跑。
  6. 链式顺序无所谓——limit(3) 放在 sorted() 前后结果不同(先截取再排序 ≠ 先排序再截取),顺序即语义。

8. 折叠自测题

❓ Q1:Stream 和 InputStream 有关系吗?和集合呢?

和 InputStream 毫无关系,纯属撞名(一个管数据加工管道,一个管文件网络字节)。和集合是"传送带与仓库"的关系:数据来自集合,Stream 本身不存数据、不改集合。

❓ Q2:为什么只有中间操作时程序什么都不输出?

惰性求值:中间操作只是登记加工步骤(返回新 Stream),没有终结操作(forEach/count/collect)触发"开火",流水线一行不执行。

❓ Q3:`s.forEach(...)` 之后想再 `s.count()` 会怎样?怎么救?

炸 IllegalStateException——流是一次性的。救法:重新 scores.stream() 开一条新流;或者第一次就用 collect 把结果装回集合,反复用集合。

❓ Q4:`filter(s -> s >= 80)` 里的 `s -> s >= 80` 是什么?为什么能这么写?

4.7 的 Lambda。filter 的参数是函数式接口 Predicate<T>(内置抽象方法"给个元素,返回真假"),Lambda 就是这个判断逻辑的简写。类型可省、单参数省括号——4.7 的省略规则原样生效。

❓ Q5:把「1~100 里的偶数、平方后、取前 5 个、装进 List」写成一行流。

List<Integer> result = IntStream.rangeClosed(1, 100).filter(n -> n % 2 == 0).map(n -> n * n).limit(5).boxed().collect(Collectors.toList()); 思路分即可:rangeClosed 开流(数值流)→ filter 筛偶 → map 平方 → limit 5 → collect 收集。答案不必背,会拆步骤就是本节毕业标准。

9. 下一步学什么

相关笔记