Stream 流概述与数据处理思想 | JavaSE
Stream 流概述与数据处理思想
一、学习目标
完成本章后,你应该能够:
- 能够解释 Stream 是什么,以及它主要解决什么问题。
- 能够区分 Java 集合与 Stream 的职责。
- 能够理解 Stream 的核心思想是“数据处理流水线”,而不是新的数据存储容器。
- 能够理解“数据源 → 中间处理 → 终结操作”的 Stream Pipeline 模型。
- 能够解释命令式集合处理与 Stream 声明式处理之间的思维差异。
- 能够理解 Stream 操作通常不会直接修改原始数据源。
- 能够解释 Stream 的惰性执行思想。
- 能够理解一个 Stream 通常只能消费一次。
- 能够为后续获取 Stream、中间操作、终结操作和收集结果建立整体知识模型。
二、核心知识
2.1 为什么需要 Stream
假设有一批姓名:
List<String> names = new ArrayList<>();
names.add("张无忌");
names.add("周芷若");
names.add("赵敏");
names.add("张强");
names.add("张三丰");
names.add("张翠山");
现在需求是:
找出所有姓“张”的名字,然后只保留长度为 3 的名字,并输出。
传统写法:
for (String name : names) {
if (name.startsWith("张")) {
if (name.length() == 3) {
System.out.println(name);
}
}
}
或者为了保存结果:
List<String> result =
new ArrayList<>();
for (String name : names) {
if (name.startsWith("张")
&& name.length() == 3) {
result.add(name);
}
}
这段代码没有错误。
但是我们需要手动负责很多流程控制:
创建循环
↓
拿出元素
↓
判断第一个条件
↓
判断第二个条件
↓
处理结果
随着需求增加:
筛选
→ 排序
→ 去重
→ 截取
→ 转换
→ 统计
代码中就会出现越来越多:
- 循环;
- 临时集合;
if;- 嵌套判断;
- 手工状态管理。
Stream 的核心价值就是:
让我们更直接地描述“数据要经历什么处理”。
2.2 Stream 风格是什么样的
同样的需求,可以表达为:
names.stream()
.filter(name ->
name.startsWith("张"))
.filter(name ->
name.length() == 3)
.forEach(System.out::println);
现在代码结构已经非常接近需求本身:
获得数据
↓
筛选姓张
↓
筛选长度为 3
↓
输出
这就是 Stream 最重要的学习价值:
从“我应该怎么控制循环”,逐渐转向“我要对数据执行什么操作”。
2.3 什么是 Stream
Stream 可以理解成:
用于对一组数据执行一系列函数式处理操作的数据处理序列。
例如:
原始数据
↓
过滤
↓
转换
↓
排序
↓
截取
↓
得到结果
需要特别注意:
Stream 不是集合。
它不是:
ArrayList
HashSet
HashMap
这种主要负责保存数据的数据结构。
Stream 更关注:
如何处理数据。
2.4 Collection 与 Stream 的区别
可以先建立一个非常重要的模型:
Collection
↓
数据在哪里、怎样存、怎样访问
Stream
↓
这些数据要怎样被处理
例如:
List<Student>
负责保存学生对象。
而:
筛选成绩 >= 60
↓
按成绩降序
↓
取前三名
↓
提取姓名
属于数据处理问题。
这正是 Stream 擅长描述的内容。
因此:
集合偏向“存储”,Stream 偏向“计算与处理”。
2.5 一个通俗比喻:仓库与流水线
可以把集合想象成:
仓库。
仓库里面放着很多商品:
商品 A
商品 B
商品 C
商品 D
而 Stream 更像:
加工流水线。
商品从仓库进入流水线:
仓库
↓
筛选合格商品
↓
按照价格排序
↓
贴标签
↓
装箱
↓
得到最终产品
流水线本身的核心职责不是长期保存商品。
而是:
定义商品经过哪些加工步骤。
所以:
Collection = 数据存储结构
Stream = 数据处理流水线
这个模型非常重要。
三、Stream Pipeline 数据处理流水线
3.1 Stream 的整体结构
一条 Stream 流水线通常可以理解成三部分:
数据源 Source
↓
中间操作 Intermediate Operations
↓
终结操作 Terminal Operation
例如:
names.stream()
.filter(name ->
name.startsWith("张"))
.filter(name ->
name.length() == 3)
.forEach(System.out::println);
可以拆成:
names
↓
数据源
stream()
↓
形成 Stream
filter(...)
↓
中间处理
filter(...)
↓
中间处理
forEach(...)
↓
终结操作
这就是:
Stream Pipeline(Stream 流水线)。
3.2 数据源 Source
Stream 必须有数据来源。
例如可能来自:
Collection
数组
生成的数据
文件中的数据
其他数据来源
本章只建立概念。
下一章:
06-06《获取 Stream》
会专门学习:
- Collection;
- Map;
- 数组;
Stream.of(...);
等不同获取方式。
所以本章暂时不要把注意力放到“怎么创建 Stream”。
重点是:
Stream 是建立在某个数据源之上的处理过程。
3.3 中间操作
例如:
.filter(...)
就是典型的中间操作。
中间操作通常完成:
过滤
排序
转换
去重
截取
……
并继续产生 Stream,使后续操作能够继续连接:
stream
.filter(...)
.filter(...)
.something(...)
.something(...)
所以它们可以像管道一样串起来。
3.4 终结操作
流水线最终通常需要:
输出
统计
查找
收集结果
计算结果
例如:
.forEach(System.out::println)
就是一个终结操作。
终结操作意味着:
开始真正消费整个 Stream Pipeline,并得到最终结果或者产生最终效果。
后面会单独学习:
- 06-08:Stream 终结操作
- 06-09:Stream 收集结果
本章只需要建立:
中间操作
↓
继续组成流水线
终结操作
↓
结束流水线并触发结果计算
四、数据处理思想
4.1 传统命令式思维
传统代码:
List<String> result =
new ArrayList<>();
for (String name : names) {
if (name.startsWith("张")
&& name.length() == 3) {
result.add(name);
}
}
开发者需要描述:
创建结果集合
遍历原集合
取出元素
执行判断
条件满足
加入新集合
关注的是:
程序应该一步一步怎么执行。
这是一种典型的命令式(Imperative)思维。
4.2 Stream 的声明式思维
Stream:
names.stream()
.filter(name ->
name.startsWith("张"))
.filter(name ->
name.length() == 3)
.forEach(System.out::println);
我们直接描述:
我要:
筛选姓张的
筛选三个字的
然后输出
而不需要自己管理:
循环变量
临时索引
遍历过程
因此 Stream 具有明显的:
声明式(Declarative)数据处理风格。
4.3 外部迭代与内部迭代
传统:
for (String name : names) {
...
}
遍历逻辑由我们显式控制。
可以理解为:
外部迭代。
我们告诉程序:
开始循环
下一条
下一条
判断
继续
而 Stream:
names.stream()
.filter(...)
.forEach(...);
我们主要提供:
筛选规则
处理规则
具体怎样遍历数据,由 Stream 框架管理。
可以理解为:
内部迭代。
这是理解 Stream 的一个重要视角。
4.4 Lambda、函数式接口与 Stream 为什么连在一起
前面连续学习:
06-01 函数式编程
06-02 Lambda
06-03 Lambda 简化
06-04 方法引用
06-05 Stream
不是偶然安排。
Stream 的很多 API 都需要:
将行为传进去。
例如:
filter(...)
需要传:
什么条件算保留?
可以写:
name -> name.startsWith("张")
又例如:
forEach(...)
需要传:
每个元素怎么处理?
可以写:
System.out::println
于是:
函数式接口
↓
规定行为类型
Lambda / 方法引用
↓
提供行为
Stream
↓
把很多行为组合成数据处理流水线
整个知识体系至此真正串了起来。
五、Stream 的重要特征
5.1 Stream 通常不负责存储数据
例如:
List<String> names
是真正的数据容器。
Stream 主要描述:
这些数据应该如何被处理。
因此不要把:
Stream<String>
简单理解成:
“另一种 List”。
它们的职责不同。
5.2 Stream 操作通常不直接改变原数据源
例如:
List<Integer> numbers =
new ArrayList<>(
List.of(3, 1, 2)
);
后面如果通过 Stream:
筛选
排序
转换
通常是在构建数据处理流程,而不是说:
调用了 Stream,原来的 List 就自动被修改了。
这是一个非常重要的思维差异。
Stream 更鼓励:
从数据源读取数据,经过一系列处理,产生结果。
而不是:
一边遍历,一边不断修改数据源本身。
5.3 Stream 具有惰性执行思想
看:
names.stream()
.filter(name -> {
System.out.println(
"正在判断:" + name
);
return name.startsWith("张");
});
很多初学者会想:
写到
filter()这里,所有名字是不是马上都检查了?
Stream 中大量中间操作具有**惰性(Lazy)**特征。
也就是说:
仅仅建立中间处理步骤,并不意味着整个数据源已经立刻完成遍历。
它更像是:
先告诉 Stream:
第一步怎么处理
第二步怎么处理
第三步怎么处理
↓
真正需要结果时
↓
启动流水线
例如:
names.stream()
.filter(name ->
name.startsWith("张"))
.forEach(System.out::println);
最后的终结操作:
forEach(...)
会推动这条流水线真正执行。
5.4 为什么需要惰性
因为当所有步骤组成完整流水线以后,Stream 才能综合考虑:
最终到底需要处理多少数据。
例如未来会学习:
筛选
↓
只取前 2 个
如果已经找到足够结果,某些场景就不一定需要继续无意义处理后面所有元素。
所以 Stream 不是简单理解成:
先完整执行 filter
生成一个新集合
再完整执行下一个操作
再生成一个新集合
真正的执行模型更加接近:
流水线式处理。
5.5 一个 Stream 通常只能消费一次
例如:
Stream<String> stream =
names.stream();
第一次:
stream.forEach(
System.out::println
);
这条 Stream 已经被终结操作消费。
不要再尝试:
stream.forEach(
System.out::println
);
因此应该建立规则:
一个 Stream Pipeline 通常只使用一次。
如果需要重新处理原数据:
names.stream()
重新建立新的 Stream 即可。
5.6 为什么不能把 Stream 当集合反复遍历
集合:
List<String> names
可以:
for (...) {
}
遍历一次。
之后仍然可以再次:
for (...) {
}
因为集合本身是数据容器。
而 Stream 表示的是:
一次数据处理过程。
因此:
Collection
↓
可以长期保存并反复访问数据
Stream
↓
一次处理流水线
这是 Collection 和 Stream 非常重要的区别。
六、Stream 代码为什么经常“一串一串”
例如:
names.stream()
.filter(...)
.filter(...)
.forEach(...);
这种形式称为:
链式调用(Method Chaining)。
它之所以能够连续调用,是因为很多中间操作会继续返回 Stream。
例如概念上:
Stream
↓
filter
↓
Stream
↓
filter
↓
Stream
↓
终结操作
于是代码结构天然形成:
source
.operation1(...)
.operation2(...)
.operation3(...);
从阅读方式看,可以从上往下读:
从 names 获取数据
→ 按条件一筛选
→ 按条件二筛选
→ 最后输出
对于复杂数据处理而言,这种表达方式通常比多层循环更加接近业务需求。
七、一个完整的思维案例
需求:
从一组学生中找出成绩及格的学生,按照成绩处理后输出。
传统思维通常首先想到:
创建循环
↓
拿学生
↓
if 判断 >= 60
↓
保存或输出
而 Stream 思维应该先把业务拆成:
数据源:学生
处理条件:
成绩 >= 60
后续操作:
排序 / 转换 / 输出
即:
学生数据
↓
过滤不及格
↓
排序
↓
提取需要的数据
↓
产生结果
真正学习 Stream 后,你应该先问:
这批数据应该依次经历哪些处理步骤?
而不是上来就问:
“我要写几个 for 循环?”
这就是 Stream 思维真正的转变。
八、常见问题
8.1 Stream 是数据结构吗?
不是传统意义上的数据存储结构。
Stream 的核心职责是:
描述和执行数据处理。
8.2 Stream 会把原集合里的数据搬走吗?
不是这种理解。
例如:
names.stream()
不会意味着:
names里面的数据被清空并转移到 Stream。
应该理解成:
基于这个数据源建立处理流水线。
8.3 使用 Stream 后还需要集合吗?
当然需要。
两者解决的问题不同:
集合:
存储和管理数据
Stream:
处理数据
实际开发中它们往往是搭配使用,而不是互相取代。
8.4 Stream 是不是只能处理集合?
不是。
Stream 可以基于多种数据源建立。
具体如何从:
- Collection;
- Map;
- 数组;
- 直接数据;
获取 Stream,将在下一章专门学习。
8.5 中间操作为什么有时候看起来“不执行”?
因为 Stream 中间操作通常具有惰性。
它们更多是在:
构建处理流水线。
直到终结操作需要结果时,流水线才真正开始消费数据。
8.6 为什么一个 Stream 通常不能用两次?
因为 Stream 代表一次数据处理流水线。
经过终结操作后,它已经被消费。
如果想重新处理:
names.stream()
重新建立一条新的 Stream。
8.7 Stream 会自动让程序更快吗?
不能这样理解。
Stream 首先解决的是:
数据处理表达方式与组合能力。
它并不意味着:
“只要改成 Stream 就一定性能更高。”
后面学习并行流时还会发现:
并行也不是越多越快。
因此现阶段不要把 Stream 的核心价值理解成“性能优化工具”。
它首先是:
一种函数式、声明式的数据处理抽象。
8.8 Stream 能完全替代 for 循环吗?
不能,也没有必要。
例如简单索引操作:
for (int i = 0; i < array.length; i++) {
...
}
可能就非常直观。
Stream 更擅长:
筛选
转换
排序
聚合
组合处理
技术选择应该服从:
可读性、语义和实际需求。
而不是认为:
“写 Stream 比写 for 高级”。
九、练习与验收
9.1 知识问答
- Stream 是什么?
- Stream 主要解决什么问题?
- Collection 和 Stream 的核心职责分别是什么?
- 为什么说 Stream 不是新的数据存储容器?
- 什么是 Stream Pipeline?
- 一条 Stream Pipeline 通常由哪三部分组成?
- 什么是中间操作?
- 什么是终结操作?
- 为什么 Stream 中间操作具有惰性特征?
- 一个 Stream 为什么通常不能重复消费?
- 什么叫命令式数据处理?
- 什么叫声明式数据处理?
- 什么叫外部迭代?
- 什么叫内部迭代?
- Lambda、函数式接口、方法引用和 Stream 之间是什么关系?
9.2 代码阅读
阅读:
List<String> names =
List.of(
"张三",
"李四",
"张无忌"
);
names.stream()
.filter(
name ->
name.startsWith("张")
)
.forEach(
System.out::println
);
回答:
- 数据源是什么?
- 哪一步属于中间操作?
- 哪一步属于终结操作?
- Lambda 表达式代表什么行为?
- 方法引用代表什么行为?
- 整条流水线可以用一句自然语言怎么描述?
阅读:
Stream<String> stream =
names.stream();
stream.forEach(
System.out::println
);
stream.forEach(
System.out::println
);
判断这段代码的设计问题,并说明为什么不应该重复使用同一个 Stream。
9.3 手写代码
给定:
List<Integer> numbers =
List.of(
1, 2, 3, 4,
5, 6, 7, 8
);
只要求设计处理思路,不要求背 API。
分别描述:
- 找出所有偶数。
- 找出所有大于 5 的数字。
- 将所有数字乘 10。
- 统计满足条件的数据。
- 将结果输出。
要求先写:
数据源
↓
处理步骤 1
↓
处理步骤 2
↓
最终结果
再思考 Stream 为什么适合表示这种流程。
9.4 Debug
下面代码的理解是否正确?
stream()会创建一个新的集合,后续filter()会删除原集合中不满足条件的元素。
请指出其中至少两个错误认识。
判断:
Stream 中写了
filter()以后,这一步一定会立即完整遍历全部数据。
是否正确?说明原因。
判断:
Stream 用完以后可以像 List 一样反复执行终结操作。
是否正确?
9.5 综合训练
假设星雨笔录后台存在:
List<Article> articles;
每个文章拥有:
title
category
viewCount
published
现在需求:
找出所有已发布的 Java 文章,按照浏览量处理,只输出标题。
暂时不要写完整 Stream API。
先把需求设计为流水线:
articles
↓
?
↓
?
↓
?
↓
title
回答:
- 数据源是什么?
- 哪些步骤属于数据筛选?
- 哪些步骤属于排序?
- 哪一步属于数据转换?
- 最终需要什么结果?
- 如果使用传统循环,需要管理哪些额外流程?
- 为什么这个问题非常适合 Stream?
9.6 本章验收
关闭资料后,能够画出:
Stream Pipeline
数据源 Source
│
▼
中间操作
│
▼
中间操作
│
▼
中间操作
│
▼
终结操作
│
▼
最终结果
并完整解释:
Collection 为什么负责“存”
Stream 为什么负责“处理”
什么是声明式数据处理
什么是内部迭代
什么是惰性执行
为什么没有终结操作时
流水线可能不会真正开始消费数据
为什么 Stream 通常只能使用一次
还需要能够说清楚整条知识链:
函数式接口
↓
描述行为类型
Lambda
↓
直接实现行为
方法引用
↓
复用已有行为
Stream
↓
把多个行为组合成数据处理流水线
能够不依赖资料完整讲清这一模型后,再进入下一章学习 Stream 的具体获取方式。