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. 常见误区
- Stream 是 I/O 流——不是。I/O 流管文件网络字节,Stream 管集合数据加工,只是撞名。
- 以为流会改原集合——绝不改。要结果就
collect装回新集合,或看 forEach 打印。 - 流可以反复用——一次性消耗品,终结操作后作废,重开一条即可。
- 在 forEach 里改外部变量当结果用——能跑但不地道(并发下还会出事),正经收集用
collect。 - 中间操作会立刻执行——惰性求值,攒着呢;没终结操作一行不跑。
- 链式顺序无所谓——
limit(3)放在sorted()前后结果不同(先截取再排序 ≠ 先排序再截取),顺序即语义。
8. 折叠自测题
和 InputStream 毫无关系,纯属撞名(一个管数据加工管道,一个管文件网络字节)。和集合是"传送带与仓库"的关系:数据来自集合,Stream 本身不存数据、不改集合。
惰性求值:中间操作只是登记加工步骤(返回新 Stream),没有终结操作(forEach/count/collect)触发"开火",流水线一行不执行。
炸 IllegalStateException——流是一次性的。救法:重新 scores.stream() 开一条新流;或者第一次就用 collect 把结果装回集合,反复用集合。
4.7 的 Lambda。filter 的参数是函数式接口 Predicate<T>(内置抽象方法"给个元素,返回真假"),Lambda 就是这个判断逻辑的简写。类型可省、单参数省括号——4.7 的省略规则原样生效。
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. 下一步学什么
- ☑ 7.9 本章小结 ✅ —— 点此复习:八节串线 + 速查 14 条 + 模板 4 段 + 八大名坑 + 毕业大题单词计数器(split + Map 计数 + Stream 排序一题串三章)。
相关笔记
- 第四章 面向对象(下)/4.7 函数式编程:Lambda 与方法引用——Stream 的发动机,本节正式转正
- 第七章 集合/7.4 集合遍历:forEach 初见与"遍历不能乱删"的老规矩
- 第七章 集合/7.7 常用工具类:Collections 排序的传统解法,对比 Stream 的 sorted
- 第六章 Java API/6.5 BigInteger和BigDecimal类:map 加工变形的思想同源——旧数据不动、产新结果