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 知识问答

  1. Stream 是什么?
  2. Stream 主要解决什么问题?
  3. Collection 和 Stream 的核心职责分别是什么?
  4. 为什么说 Stream 不是新的数据存储容器?
  5. 什么是 Stream Pipeline?
  6. 一条 Stream Pipeline 通常由哪三部分组成?
  7. 什么是中间操作?
  8. 什么是终结操作?
  9. 为什么 Stream 中间操作具有惰性特征?
  10. 一个 Stream 为什么通常不能重复消费?
  11. 什么叫命令式数据处理?
  12. 什么叫声明式数据处理?
  13. 什么叫外部迭代?
  14. 什么叫内部迭代?
  15. Lambda、函数式接口、方法引用和 Stream 之间是什么关系?

9.2 代码阅读

阅读:

List<String> names =
        List.of(
                "张三",
                "李四",
                "张无忌"
        );

names.stream()
        .filter(
                name ->
                        name.startsWith("张")
        )
        .forEach(
                System.out::println
        );

回答:

  1. 数据源是什么?
  2. 哪一步属于中间操作?
  3. 哪一步属于终结操作?
  4. Lambda 表达式代表什么行为?
  5. 方法引用代表什么行为?
  6. 整条流水线可以用一句自然语言怎么描述?

阅读:

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。

分别描述:

  1. 找出所有偶数。
  2. 找出所有大于 5 的数字。
  3. 将所有数字乘 10。
  4. 统计满足条件的数据。
  5. 将结果输出。

要求先写:

数据源
↓
处理步骤 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

回答:

  1. 数据源是什么?
  2. 哪些步骤属于数据筛选?
  3. 哪些步骤属于排序?
  4. 哪一步属于数据转换?
  5. 最终需要什么结果?
  6. 如果使用传统循环,需要管理哪些额外流程?
  7. 为什么这个问题非常适合 Stream?

9.6 本章验收

关闭资料后,能够画出:

               Stream Pipeline

数据源 Source
     │
     ▼
中间操作
     │
     ▼
中间操作
     │
     ▼
中间操作
     │
     ▼
终结操作
     │
     ▼
最终结果

并完整解释:

Collection 为什么负责“存”

Stream 为什么负责“处理”

什么是声明式数据处理

什么是内部迭代

什么是惰性执行

为什么没有终结操作时
流水线可能不会真正开始消费数据

为什么 Stream 通常只能使用一次

还需要能够说清楚整条知识链:

函数式接口
     ↓
描述行为类型

Lambda
     ↓
直接实现行为

方法引用
     ↓
复用已有行为

Stream
     ↓
把多个行为组合成数据处理流水线

能够不依赖资料完整讲清这一模型后,再进入下一章学习 Stream 的具体获取方式。