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 知识问答
- 什么是 Stream 终结操作?
- 中间操作与终结操作有什么本质区别?
- 为什么终结操作会触发 Stream Pipeline?
forEach()使用什么函数式接口?count()返回什么类型?max()、min()为什么返回 Optional?- Optional 为什么比直接返回 null 更能表达语义?
findFirst()与findAny()有什么区别?anyMatch()、allMatch()、noneMatch()分别解决什么问题?- 什么叫短路终结操作?
- 什么是 Reduction?
reduce(identity, accumulator)中 identity 是什么?- 为什么一个 Stream 终结以后不能继续使用?
16.2 代码阅读
long count =
Stream.of(
1,
2,
3,
4,
5
)
.filter(
n ->
n % 2 == 0
)
.count();
回答:
- 最终结果是什么?
filter是什么操作?count是什么操作?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;
分别完成:
- 统计学生人数。
- 统计及格人数。
- 找成绩最高学生。
- 找年龄最小学生。
- 判断是否存在成绩 100 分的学生。
- 判断是否所有学生成年。
- 判断是否无人不及格。
- 找第一个来自北京的学生。
- 使用
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;
需要完成:
- 统计已经发布的文章数量。
- 找浏览量最高的文章。
- 判断是否存在浏览量超过 10000 的文章。
- 判断是否所有文章都拥有标题。
- 找第一篇 Java 类文章。
- 使用
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 的元素收集成新的数据结构”。