Stream 终结操作 | JavaSE

Stream 终结操作

一、学习目标

完成本章后,你应该能够:

  • 能够解释什么是 Stream 终结操作(Terminal Operation)。
  • 能够理解终结操作为什么会触发 Stream Pipeline 的实际消费。
  • 能够使用 forEach() 遍历处理元素。
  • 能够使用 count() 统计元素数量。
  • 能够使用 max()min() 获取最大值和最小值。
  • 能够正确理解和处理 Optional<T>
  • 能够使用 findFirst()findAny() 查找元素。
  • 能够使用 anyMatch()allMatch()noneMatch() 完成条件判断。
  • 能够使用 reduce() 完成聚合计算。
  • 能够理解短路终结操作(Short-circuiting Terminal Operation)。
  • 能够避免 Stream 被终结后再次重复使用。

二、核心知识

2.1 什么是终结操作

上一章我们已经能够构造:

students.stream()
        .filter(...)
        .sorted(...)
        .limit(...)
        .map(...);

但是:

stream()
↓
filter
↓
sorted
↓
map

这些步骤主要是在:

构建 Stream Pipeline。

真正需要:

输出结果
统计数量
寻找最大值
判断是否存在
聚合计算

时,需要一个操作结束整条流水线。

这种操作叫:

终结操作(Terminal Operation)。

例如:

students.stream()
        .filter(
                student ->
                        student.getScore() >= 60
        )
        .count();

这里:

count()

就是终结操作。


2.2 中间操作与终结操作

可以建立最重要的区别:

中间操作
Intermediate Operation

Stream → Stream

例如:

filter()
map()
sorted()
distinct()
limit()
skip()

而终结操作通常:

Stream → 最终结果

结果可能是:

void
long
boolean
Optional<T>
T
数组
集合
Map
……

例如:

count()

得到:

long

而:

max(...)

得到:

Optional<T>

因此一旦调用终结操作:

这条 Stream Pipeline 就结束了。


2.3 为什么终结操作很重要

Stream 中间操作通常具有惰性。

例如:

students.stream()
        .filter(student -> {
            System.out.println(
                    "正在判断:" + student
            );

            return student.getScore() >= 60;
        });

如果后面没有终结操作:

filter 的业务逻辑通常不会因为你“写出来”就立即完整执行。

加入:

students.stream()
        .filter(student -> {
            System.out.println(
                    "正在判断:" + student
            );

            return student.getScore() >= 60;
        })
        .count();

count() 需要得到最终数量,于是会推动流水线完成必要的数据处理。

所以可以理解:

中间操作
    ↓
定义流水线

终结操作
    ↓
消费流水线
    ↓
得到最终结果

三、forEach:遍历处理

3.1 基本形式

原课程首先给出:

void forEach(Consumer action)

例如:

List<String> names =
        List.of(
                "Java",
                "MySQL",
                "Spring"
        );

names.stream()
        .forEach(
                name ->
                        System.out.println(name)
        );

使用方法引用:

names.stream()
        .forEach(
                System.out::println
        );

3.2 为什么 forEach 是终结操作

因为:

forEach(...)

不会再返回一个新的 Stream。

例如不能继续:

// 错误思路
names.stream()
        .forEach(System.out::println)
        .filter(...);

forEach() 已经把元素消费掉了。

所以:

Stream<String>
     ↓
forEach
     ↓
执行 Consumer 行为
     ↓
void

3.3 Collection.forEach 和 Stream.forEach

注意两种写法:

names.forEach(
        System.out::println
);

和:

names.stream()
        .forEach(
                System.out::println
        );

都可以遍历。

但:

names.forEach(...)

属于:

Iterable / Collection 体系的遍历。

而:

names.stream().forEach(...)

属于:

Stream Pipeline 的终结操作。

不要因为方法名字相同就认为它们完全是同一个 API。


3.4 forEachOrdered

Java Stream 还提供:

forEachOrdered(...)

它主要用于:

当 Stream 存在 encounter order 时,按照该顺序执行操作。

在普通顺序流中:

forEach()

通常已经符合直觉顺序。

但并行 Stream 中:

forEach()

不保证保持 encounter order。

如果确实需要顺序,可以使用:

forEachOrdered()

现阶段只需要建立这个区别,不需要急着使用并行流。


四、count:统计数量

4.1 基本使用

原课程:

long count()

例如:

long count =
        teachers.stream()
                .filter(
                        teacher ->
                                teacher.getSalary()
                                        > 15000
                )
                .count();

得到:

工资大于 15000 的教师数量。


4.2 返回值为什么是 long

count() 返回:

long

所以不要写:

// 不推荐
int count =
        stream.count();

正确:

long count =
        stream.count();

4.3 典型业务

例如:

long passedCount =
        students.stream()
                .filter(
                        student ->
                                student.getScore() >= 60
                )
                .count();

自然语言就是:

学生
 ↓
筛选及格
 ↓
统计人数

五、max 与 min

5.1 为什么需要 Comparator

原课程提供:

Optional<T> max(Comparator comparator)

Optional<T> min(Comparator comparator)

例如:

Optional<Teacher> max =
        teachers.stream()
                .max(
                        Comparator
                                .comparingDouble(
                                        Teacher::getSalary
                                )
                );

含义:

按工资比较,找最大的 Teacher。


5.2 max

例如:

Optional<Student> maxScoreStudent =
        students.stream()
                .max(
                        Comparator
                                .comparingDouble(
                                        Student::getScore
                                )
                );

注意:

max()

找的是:

根据 Comparator 规则最大的元素。

Comparator 决定:

“什么叫大”

5.3 min

同理:

Optional<Student> minScoreStudent =
        students.stream()
                .min(
                        Comparator
                                .comparingDouble(
                                        Student::getScore
                                )
                );

得到:

分数最低的学生。


5.4 一个容易混淆的问题

如果写:

Comparator<Student> comparator =
        Comparator
                .comparingDouble(
                        Student::getScore
                )
                .reversed();

然后:

students.stream()
        .max(comparator);

此时:

Comparator 已经反转。

因此 max() 找到的“最大”是按照这个反转规则判断的。

这很容易把自己绕晕。

实际开发中:

如果只是找成绩最高者,Comparator 保持自然的升序比较逻辑通常最清楚。


六、为什么 max/min 返回 Optional

6.1 空 Stream 怎么办

假设:

Stream<Student> stream =
        Stream.empty();

问:

谁是最大值?

根本没有元素。

所以:

max()

无法永远保证一定存在结果。

如果直接返回:

Student

就不得不面对:

null

之类的问题。

因此 Java 返回:

Optional<Student>

6.2 Optional 是什么

Optional<T> 可以理解成:

一个可能有值,也可能没有值的结果容器。

例如:

Optional<Student>

有值:
Student

或

没有值:
empty

它显式表达:

“这个操作可能没有结果。”


6.3 不要无脑 get()

原课程示例中:

Teacher maxTeacher =
        max.get();

在确定 Stream 一定非空时可以取得值。

但是:

get()

如果 Optional 为空,会抛出异常。

因此实际代码中应该根据业务语义选择更清晰的方式。


6.4 ifPresent

例如:

maxScoreStudent.ifPresent(
        student ->
                System.out.println(
                        "最高分学生:"
                                + student
                )
);

含义:

有值
 ↓
执行

没值
 ↓
什么也不做

6.5 orElse

例如:

Student student =
        maxScoreStudent.orElse(
                defaultStudent
        );

含义:

有最大值
 ↓
返回最大值

没有最大值
 ↓
返回默认对象

6.6 orElseThrow

如果业务上:

没有学生就是异常情况。

可以:

Student student =
        maxScoreStudent
                .orElseThrow();

或者:

Student student =
        maxScoreStudent
                .orElseThrow(
                        () ->
                                new IllegalStateException(
                                        "学生集合为空"
                                )
                );

所以比单纯记:

optional.get()

更重要的是:

根据“没有值意味着什么”决定怎么处理 Optional。


七、findFirst 与 findAny

这部分属于 Java 21 官方 Stream API 补充。

7.1 findFirst

Optional<T> findFirst()

表示:

找到 Stream 中的第一个元素。

例如:

Optional<Student> result =
        students.stream()
                .filter(
                        student ->
                                student.getScore() >= 90
                )
                .findFirst();

自然语言:

找第一个
成绩 >= 90 的学生

7.2 findFirst 是短路操作

假设:

学生 1:70
学生 2:95
学生 3:98
学生 4:99

需求:

filter(score >= 90)
findFirst()

找到第二个学生以后:

已经能够确定结果。

不必为了最终结果继续寻找后面所有元素。

因此:

findFirst()

是:

短路终结操作。


7.3 findAny

Optional<T> findAny()

表示:

可以返回 Stream 中某个元素。

在顺序 Stream 中经常看起来和 findFirst() 类似。

但它的设计允许:

在并行 Stream 中更加自由地选择某个元素,以换取并行执行空间。

所以:

需要稳定的“第一个”
→ findFirst()

只需要“随便找到一个”
→ findAny()

八、anyMatch、allMatch、noneMatch

这三个 API 都使用:

Predicate<T>

即:

T → boolean

8.1 anyMatch

boolean anyMatch(Predicate predicate)

表示:

是否至少有一个元素满足条件。

例如:

boolean existsExcellent =
        students.stream()
                .anyMatch(
                        student ->
                                student.getScore()
                                        >= 90
                );

可以读作:

有没有学生成绩达到 90?


8.2 短路机制

如果第一个学生:

score = 95

那么:

anyMatch(score >= 90)

已经可以确定:

true

没必要继续判断全部学生。

所以:

anyMatch()

也是短路终结操作。


8.3 allMatch

boolean allMatch(Predicate predicate)

表示:

是否所有元素都满足条件。

例如:

boolean allPassed =
        students.stream()
                .allMatch(
                        student ->
                                student.getScore()
                                        >= 60
                );

表示:

是否全员及格?

只要发现一个:

score < 60

立即可以确定:

false

8.4 noneMatch

boolean noneMatch(Predicate predicate)

表示:

是否没有任何元素满足条件。

例如:

boolean nobodyFailed =
        students.stream()
                .noneMatch(
                        student ->
                                student.getScore()
                                        < 60
                );

自然语言:

是否没有任何人不及格?


8.5 三种 Match 总结

anyMatch
至少一个满足?

allMatch
全部满足?

noneMatch
一个都不满足?

例如:

学生成绩
70 80 90

对于:

score >= 60

得到:

anyMatch  → true
allMatch  → true
noneMatch → false

九、空 Stream 与 match

这里有一个容易出现在面试和代码阅读中的细节。

对于空 Stream:

Stream.empty()

执行:

anyMatch(...)

得到:

false

而:

allMatch(...)

得到:

true

noneMatch(...)

true

初学者可能觉得:

“一个元素都没有,怎么能说全部满足?”

从逻辑上理解:

没有找到一个违反条件的元素。

因此空集合上的 allMatch 属于逻辑中的“空真(vacuous truth)”。

这个知识不用死抠数学术语,但应当知道 API 行为。


十、reduce:归约

10.1 什么叫 Reduction

例如有:

1
2
3
4
5

最后希望变成:

15

这是:

多个元素
  ↓
不断组合
  ↓
一个最终值

这种操作叫:

归约(Reduction)。


10.2 求和

可以写:

int sum =
        Stream.of(
                1,
                2,
                3,
                4,
                5
        )
        .reduce(
                0,
                (a, b) ->
                        a + b
        );

更简洁:

int sum =
        Stream.of(
                1,
                2,
                3,
                4,
                5
        )
        .reduce(
                0,
                Integer::sum
        );

结果:

15

10.3 reduce 是怎么执行的

可以近似理解:

identity = 0

0 + 1 = 1
1 + 2 = 3
3 + 3 = 6
6 + 4 = 10
10 + 5 = 15

最终:

15

10.4 identity 是什么

在:

reduce(
        0,
        Integer::sum
)

中:

0

叫:

identity(恒等值 / 单位元)。

对于加法:

0 + x = x

所以 0 合适。

对于乘法:

1 × x = x

因此乘积可以:

int product =
        numbers.stream()
                .reduce(
                        1,
                        (a, b) ->
                                a * b
                );

10.5 不带 identity 的 reduce

还可以:

Optional<Integer> result =
        numbers.stream()
                .reduce(
                        Integer::sum
                );

为什么返回:

Optional<Integer>

因为如果 Stream 为空:

没有任何元素可以作为最终归约结果。

所以需要 Optional。


10.6 reduce 可以解决什么

例如:

求和
求积
最值
字符串拼接
组合状态

都可以视为 reduction。

但并不意味着:

所有聚合都要手写 reduce。

如果已经存在:

count()
max()
min()

等语义明确的方法,优先使用对应 API 通常更清晰。


十一、终结操作分类

现在可以建立完整模型:

消费元素

forEach()
forEachOrdered()

统计

count()

最值

max()
min()

查找

findFirst()
findAny()

判断

anyMatch()
allMatch()
noneMatch()

聚合

reduce()

收集

还有:

collect()
toArray()
toList()

它们本身也是终结操作。

但由于冻结课程专门划分:

06-09《Stream 收集结果》

所以下一章集中展开。


十二、短路终结操作

12.1 什么叫短路

短路的核心是:

不一定需要消费整个 Stream 就能够确定最终结果。

例如:

anyMatch(...)

发现第一个满足条件的元素:

已经知道 true

就可以停止继续判断。


12.2 常见短路终结操作

包括:

anyMatch
allMatch
noneMatch
findFirst
findAny

它们都可能:

在获得足够信息后提前结束。


12.3 count 不是简单的“每个元素一定执行一次”

这一点非常重要。

不要认为:

stream
        .peek(System.out::println)
        .count();

必然一定会打印全部元素。

Java Stream 实现可能根据数据源已知大小等信息直接计算 count(),从而省略一些没有影响最终结果的遍历步骤。

因此再次强调:

不要依赖 Stream 中间操作的副作用来实现核心业务。

尤其是:

peek()

应该主要用于 Debug / 观察。


十三、Stream 终结之后不能继续使用

例如:

Stream<String> stream =
        names.stream();

long count =
        stream.count();

此时 Stream 已经被消费。

不要继续:

// 错误
stream.forEach(
        System.out::println
);

通常会出现:

IllegalStateException

正确思路:

long count =
        names.stream()
                .count();

names.stream()
        .forEach(
                System.out::println
        );

每次重新:

names.stream()

建立新的 Pipeline。


十四、实践应用:学生统计

假设:

List<Student> students;

统计及格人数

long passedCount =
        students.stream()
                .filter(
                        student ->
                                student.getScore() >= 60
                )
                .count();

找最高分

Optional<Student> topStudent =
        students.stream()
                .max(
                        Comparator
                                .comparingDouble(
                                        Student::getScore
                                )
                );

判断是否有人不及格

boolean hasFailed =
        students.stream()
                .anyMatch(
                        student ->
                                student.getScore() < 60
                );

判断是否全部成年

boolean allAdult =
        students.stream()
                .allMatch(
                        student ->
                                student.getAge() >= 18
                );

找第一个优秀学生

Optional<Student> firstExcellent =
        students.stream()
                .filter(
                        student ->
                                student.getScore() >= 90
                )
                .findFirst();

求总成绩

double totalScore =
        students.stream()
                .map(
                        Student::getScore
                )
                .reduce(
                        0.0,
                        Double::sum
                );

整套逻辑已经形成:

数据
 ↓
中间处理
 ↓
终结操作
 ↓
最终结果

十五、常见问题

15.1 forEach 是中间操作吗?

不是。

它是终结操作。

调用以后不能继续:

.filter(...)
.map(...)

15.2 count 返回 int 吗?

不是。

返回:

long

15.3 max 返回 Student 吗?

不是直接返回:

Student

而是:

Optional<Student>

因为 Stream 可能为空。


15.4 Optional 可以直接 get 吗?

可以调用,但不应该形成:

“看到 Optional 就无脑 get()”

的习惯。

应该根据业务语义选择:

ifPresent()
orElse()
orElseThrow()

等方式。


15.5 max 和 sorted().findFirst() 是一回事吗?

都可能找到最大元素,但语义和代价不同。

如果目标只是:

找最大值。

直接:

max(comparator)

更加直接。

没有必要为了找一个最大值:

完整排序所有数据
→
再拿第一个

15.6 anyMatch 为什么可能不遍历所有元素?

因为它是短路操作。

找到一个满足条件的元素以后:

true

已经确定。


15.7 findAny 和 findFirst 一样吗?

不是。

findFirst()

强调 encounter order 中的第一个。

findAny()

允许返回任意元素,更适合不要求稳定顺序的场景,特别是并行 Stream。


15.8 reduce 和 collect 是同一个东西吗?

都是 Reduction 思想的一部分,但重点不同。

reduce
 ↓
通常把元素组合成一个不可变式结果值

collect
 ↓
Mutable Reduction
通常把元素累积到 List / Set / Map 等可变容器

collect() 下一章专门学习。


十六、练习与验收

16.1 知识问答

  1. 什么是 Stream 终结操作?
  2. 中间操作与终结操作有什么本质区别?
  3. 为什么终结操作会触发 Stream Pipeline?
  4. forEach() 使用什么函数式接口?
  5. count() 返回什么类型?
  6. max()min() 为什么返回 Optional?
  7. Optional 为什么比直接返回 null 更能表达语义?
  8. findFirst()findAny() 有什么区别?
  9. anyMatch()allMatch()noneMatch() 分别解决什么问题?
  10. 什么叫短路终结操作?
  11. 什么是 Reduction?
  12. reduce(identity, accumulator) 中 identity 是什么?
  13. 为什么一个 Stream 终结以后不能继续使用?

16.2 代码阅读

long count =
        Stream.of(
                1,
                2,
                3,
                4,
                5
        )
        .filter(
                n ->
                        n % 2 == 0
        )
        .count();

回答:

  1. 最终结果是什么?
  2. filter 是什么操作?
  3. count 是什么操作?
  4. count 返回什么类型?

分析:

boolean result =
        Stream.of(
                70,
                80,
                95,
                50
        )
        .anyMatch(
                score ->
                        score >= 90
        );

最终结果是什么?

理论上是否必须判断最后一个 50 才能得到结果?


分析:

int result =
        Stream.of(
                1,
                2,
                3,
                4
        )
        .reduce(
                0,
                Integer::sum
        );

逐步写出归约过程。

16.3 手写代码

针对:

List<Student> students;

分别完成:

  1. 统计学生人数。
  2. 统计及格人数。
  3. 找成绩最高学生。
  4. 找年龄最小学生。
  5. 判断是否存在成绩 100 分的学生。
  6. 判断是否所有学生成年。
  7. 判断是否无人不及格。
  8. 找第一个来自北京的学生。
  9. 使用 reduce 求所有学生总成绩。

16.4 Debug

下面代码有什么问题?

Stream<String> stream =
        names.stream();

long count =
        stream.count();

stream.forEach(
        System.out::println
);

说明原因并修改。


判断:

Student student =
        students.stream()
                .max(
                        Comparator
                                .comparingDouble(
                                        Student::getScore
                                )
                )
                .get();

代码为什么存在风险?

给出至少两种更加明确的处理方式。

16.5 综合训练

假设星雨笔录后台存在:

List<Article> articles;

需要完成:

  1. 统计已经发布的文章数量。
  2. 找浏览量最高的文章。
  3. 判断是否存在浏览量超过 10000 的文章。
  4. 判断是否所有文章都拥有标题。
  5. 找第一篇 Java 类文章。
  6. 使用 reduce 计算全部文章浏览量总和。

每个需求都需要说明:

中间操作是什么?
终结操作是什么?
最终结果类型是什么?

16.6 本章验收

闭卷画出:

Stream
 ├─ forEach      → void
 ├─ count        → long
 ├─ max/min      → Optional<T>
 ├─ findFirst    → Optional<T>
 ├─ findAny      → Optional<T>
 ├─ anyMatch     → boolean
 ├─ allMatch     → boolean
 ├─ noneMatch    → boolean
 └─ reduce       → T / Optional<T>

并能够解释:

为什么 Optional 存在?

为什么 Match 和 Find 可以短路?

为什么 reduce 是归约?

为什么终结后的 Stream 不能再使用?

达到这个标准后,再进入“怎样把 Stream 的元素收集成新的数据结构”。