Stream 中间操作 | JavaSE
Stream 中间操作
一、学习目标
完成本章后,你应该能够:
- 能够解释什么是 Stream 中间操作(Intermediate Operation)。
- 能够使用
filter()根据条件过滤数据。 - 能够使用
map()完成元素转换。 - 能够使用
distinct()去除重复元素。 - 能够使用
sorted()和 Comparator 完成排序。 - 能够使用
limit()获取前若干元素。 - 能够使用
skip()跳过前若干元素。 - 能够将多个中间操作组合成完整 Stream Pipeline。
- 能够理解无状态中间操作与有状态中间操作的基本区别。
- 能够理解
Stream.concat()是流组合能力,而不是普通实例中间操作。 - 能够认识 Java 21 中
flatMap()、peek()、takeWhile()、dropWhile()等补充能力。
二、核心知识
2.1 什么是中间操作
上一章可以得到:
Stream<String> stream =
names.stream();
现在我们需要开始处理数据。
例如:
names.stream()
.filter(...)
.map(...)
.sorted(...)
.limit(...)
这些操作的共同特点是:
接收一个 Stream,并继续产生新的 Stream,从而能够继续组成流水线。
例如:
Stream<String>
↓
filter()
↓
Stream<String>
↓
map()
↓
Stream<Integer>
↓
sorted()
↓
Stream<Integer>
这种处于:
数据源
和
终结操作
之间的 Stream 处理步骤,就叫:
中间操作(Intermediate Operation)。
2.2 中间操作不会结束流水线
例如:
Stream<String> result =
names.stream()
.filter(
name ->
name.startsWith("张")
);
filter() 之后仍然得到:
Stream<String>
因此还可以:
result
.filter(...)
.map(...)
.sorted(...);
这就是 Stream 可以进行链式调用的基础。
2.3 原课程核心中间操作
本章主线:
| 方法 | 核心作用 |
| ------------ | ------------ |
| filter() | 过滤 |
| map() | 映射 / 转换 |
| distinct() | 去重 |
| sorted() | 排序 |
| limit() | 取前若干个 |
| skip() | 跳过前若干个 |
原课程另外给出:
Stream.concat(a, b)
用于合并两个 Stream。
后面单独说明。
三、filter:过滤
3.1 基本作用
filter():
根据条件决定元素是否继续留在 Stream 中。
基本形式:
stream.filter(predicate)
其参数是:
Predicate<T>
上一阶段已经学习:
Predicate<T>
T → boolean
所以:
filter()
正好需要一个:
对元素进行真假判断的行为。
3.2 姓张的名字
例如:
List<String> names =
List.of(
"张无忌",
"周芷若",
"赵敏",
"张强",
"张三丰",
"张翠山"
);
筛选:
names.stream()
.filter(
name ->
name.startsWith("张")
);
逻辑:
张无忌 → true → 保留
周芷若 → false → 丢弃
赵敏 → false → 丢弃
张强 → true → 保留
...
所以:
Predicate返回true的元素保留下来。
3.3 多条件过滤
可以:
names.stream()
.filter(
name ->
name.startsWith("张")
&& name.length() == 3
);
也可以拆成:
names.stream()
.filter(
name ->
name.startsWith("张")
)
.filter(
name ->
name.length() == 3
);
两种方式在简单场景都可以。
后者往往更容易表达:
条件 1
↓
条件 2
的流水线思维。
3.4 对象过滤
例如:
students.stream()
.filter(
student ->
student.getScore() >= 60
);
表达:
只让及格学生继续向下游流动。
四、map:映射与转换
4.1 filter 与 map 的根本区别
filter():
决定一个元素“留还是不留”。
map():
决定一个元素“变成什么”。
例如:
filter
Student
↓
满足条件?
↓
仍然是 Student
而:
map
Student
↓
getName()
↓
String
所以 map() 非常重要。
4.2 基本形式
stream.map(mapper)
它使用:
Function<T, R>
也就是:
T → R
4.3 字符串转长度
例如:
Stream<String> names =
Stream.of(
"Java",
"MySQL",
"Spring"
);
可以:
Stream<Integer> lengths =
names.map(
name ->
name.length()
);
发生了类型变化:
Stream<String>
↓
map(String::length)
↓
Stream<Integer>
4.4 方法引用
前一章已经学习方法引用:
name -> name.length()
可以:
String::length
因此:
Stream<Integer> lengths =
names.map(
String::length
);
4.5 Student 映射为姓名
非常典型:
Stream<String> names =
students.stream()
.map(
Student::getName
);
过程:
Student
↓
getName()
↓
String
注意:
map()可以改变 Stream 的元素类型。
4.6 map 不是修改原对象的同义词
例如:
students.stream()
.map(Student::getName);
不是:
把原来的
Student对象修改成 String。
而是:
根据每个 Student 计算一个新结果,组成新的 Stream。
原来的:
List<Student>
依然还是:
List<Student>
五、distinct:去重
5.1 基本作用
stream.distinct()
用于:
去掉 Stream 中重复的元素。
例如:
Stream.of(
"Java",
"MySQL",
"Java",
"Spring",
"MySQL"
)
.distinct();
结果逻辑上:
Java
MySQL
Spring
5.2 distinct 根据什么判断重复
对于对象 Stream,distinct() 的重复语义基于:
Object.equals()
例如字符串已经正确重写了:
equals()
所以相同内容 String 能正确去重。
5.3 自定义对象
假设:
Student a =
new Student(
"张三",
20
);
Student b =
new Student(
"张三",
20
);
如果 Student 没有根据业务属性重写:
equals()
那么:
a.equals(b)
默认可能仍为:
false
此时:
Stream.of(a, b)
.distinct()
不一定得到你业务上期待的:
“只剩一个张三”。
因此自定义对象去重的核心仍然回到前面学过的:
对象相等语义。
5.4 有序 Stream 的稳定性
对于具有 encounter order 的 Stream:
A
B
A
C
B
执行:
distinct()
会保留第一次遇到的重复元素:
A
B
C
所以在有序 Stream 中,去重保持稳定的遇见顺序。
六、sorted:排序
6.1 自然排序
stream.sorted()
根据元素的:
自然顺序(Natural Order)
进行排序。
例如:
Stream.of(
88.6,
66.6,
99.6,
77.6
)
.sorted();
得到升序:
66.6
77.6
88.6
99.6
6.2 sorted() 的前提
自然排序要求元素能够比较。
通常意味着对象需要实现:
Comparable
例如:
IntegerDoubleString
都已经拥有自然排序能力。
如果元素没有可用的自然顺序,直接:
sorted()
可能在流水线真正执行时产生:
ClassCastException
6.3 Comparator 自定义排序
更灵活:
sorted(Comparator)
例如成绩降序:
scores.stream()
.sorted(
(s1, s2) ->
Double.compare(
s2,
s1
)
);
因为:
s2 与 s1
反过来比较
得到降序。
6.4 Comparator 方法
还可以:
scores.stream()
.sorted(
Comparator.reverseOrder()
);
对象:
students.stream()
.sorted(
Comparator.comparingDouble(
Student::getScore
)
);
成绩降序:
students.stream()
.sorted(
Comparator
.comparingDouble(
Student::getScore
)
.reversed()
);
这与前面 Comparator 章节已经形成完整连接。
七、limit:限制数量
7.1 基本作用
limit(n)
表示:
Stream 最多只保留前
n个元素。
例如:
Stream.of(
"A",
"B",
"C",
"D"
)
.limit(2);
结果:
A
B
7.2 排名前两名
这是非常典型的组合:
scores.stream()
.sorted(
Comparator.reverseOrder()
)
.limit(2);
语义:
成绩
↓
从高到低排序
↓
只取前 2
这已经非常接近自然语言:
获取成绩最高的两个人。
7.3 limit(0)
limit(0)
得到空 Stream。
7.4 负数
不要写:
limit(-1)
maxSize 为负数属于非法参数。
八、skip:跳过元素
8.1 基本作用
skip(n)
表示:
丢掉前
n个元素,保留剩余元素。
例如:
Stream.of(
"A",
"B",
"C",
"D"
)
.skip(2);
剩余:
C
D
8.2 排名第三名以后
例如:
scores.stream()
.sorted(
Comparator.reverseOrder()
)
.skip(2);
语义:
成绩降序
↓
跳过前两名
↓
得到第三名及之后
8.3 skip 大于元素数量
例如原 Stream 只有:
3 个元素
执行:
skip(100)
结果就是:
空 Stream
8.4 skip 负数
skip(-1)
同样属于非法参数。
九、limit 与 skip 组合
9.1 分页思想
假设一页:
10 条数据
第 3 页需要:
跳过前 20 条
取 10 条
可以建立:
stream
.skip(20)
.limit(10);
一般分页模型:
pageSize = 每页数量
pageNum = 页码
skip:
(pageNum - 1) * pageSize
limit:
pageSize
例如:
students.stream()
.skip(
(long) (pageNum - 1)
* pageSize
)
.limit(pageSize);
这里帮助理解:
skip
+
limit
可以表达“区间截取”。
实际数据库分页通常应该由数据库查询完成,而不是把全部数据加载进 Java 后再 Stream 分页。
十、多个中间操作组成 Pipeline
现在可以完整组合:
students.stream()
.filter(
student ->
student.getScore() >= 60
)
.sorted(
Comparator
.comparingDouble(
Student::getScore
)
.reversed()
)
.limit(3)
.map(
Student::getName
);
从上向下读:
学生
↓
只要及格的
↓
按成绩从高到低
↓
只取前三
↓
把 Student 转为姓名 String
最终类型变化:
List<Student>
↓
Stream<Student>
↓ filter
Stream<Student>
↓ sorted
Stream<Student>
↓ limit
Stream<Student>
↓ map
Stream<String>
这是掌握 Stream 最重要的能力之一:
看懂每一步以后 Stream 中到底装的是什么类型。
十一、无状态与有状态中间操作
11.1 filter / map
例如:
filter()
map()
处理当前元素时,通常不需要先知道 Stream 中其他所有元素。
例如:
name -> name.length()
只需要当前 name。
这类操作可以理解为:
无状态中间操作(Stateless Intermediate Operation)。
11.2 sorted / distinct
排序:
sorted()
如果不知道其他元素,无法判断当前元素最终应该放在哪里。
去重:
distinct()
至少需要知道:
之前是否已经见过等价元素。
因此属于:
有状态中间操作(Stateful Intermediate Operation)。
11.3 limit / skip
limit() 和 skip() 同样需要知道元素处于流水线中的位置或数量状态。
因此官方也将它们归为有状态中间操作。
其中:
limit()
还是一个:
短路型有状态中间操作(Short-circuiting Stateful Intermediate Operation)
因为满足数量以后,某些流水线不需要继续生产更多元素。
十二、Stream.concat:合并两个 Stream
原课程将:
Stream.concat(a, b)
与中间方法放在同一组进行教学。
它的作用确实与 Stream 处理密切相关:
Stream<String> a =
Stream.of(
"A",
"B"
);
Stream<String> b =
Stream.of(
"C",
"D"
);
Stream<String> result =
Stream.concat(a, b);
结果顺序:
A
B
C
D
即:
第一个 Stream
↓
完整元素
然后
↓
第二个 Stream
但是从 API 分类上需要更准确:
concat()是Stream的静态方法,用于创建一个惰性拼接的新 Stream,而不是stream.filter()这种实例中间操作。
所以不要写:
// stream.concat(other)
而应该:
Stream.concat(
stream1,
stream2
);
十三、JDK 21 官方补充:flatMap
13.1 为什么 map 有时不够
假设:
List<List<String>> groups;
如果:
groups.stream()
.map(
List::stream
);
得到的是:
Stream<Stream<String>>
也就是:
一条 Stream
里面每个元素
又是一条 Stream
13.2 flatMap 的作用
flatMap() 可以理解:
map
+
flatten
也就是:
先映射,再把多层 Stream 展平。
例如:
groups.stream()
.flatMap(
List::stream
);
结果:
Stream<String>
示意:
[
[A, B],
[C, D]
]
↓ flatMap
A
B
C
D
这是处理嵌套集合非常重要的 Stream 能力。
十四、JDK 21 官方补充:peek
14.1 作用
peek(action)
会在元素经过流水线时执行一个附加动作,并继续返回 Stream。
例如调试:
students.stream()
.filter(
s ->
s.getScore() >= 60
)
.peek(
s ->
System.out.println(
"过滤后:" + s
)
)
.map(
Student::getName
);
14.2 peek 的正确定位
peek() 最典型的用途是:
观察和 Debug Stream Pipeline。
不要把:
peek()
当作执行复杂业务副作用的主要工具。
因为 Stream 可能存在:
- 惰性
- 短路
- 优化
- 并行
某些元素甚至可能不会实际经过某个 peek()。
所以:
peek()更适合看数据流经某一步时是什么状态。
十五、JDK 21 官方补充:takeWhile 与 dropWhile
这两个方法从 Java 9 开始存在。
15.1 takeWhile
对于有序 Stream:
takeWhile(predicate)
表示:
从开头连续获取满足条件的最长前缀,一旦遇到第一个不满足条件的元素就停止。
例如:
Stream.of(
2,
4,
6,
7,
8,
10
)
.takeWhile(
number ->
number % 2 == 0
);
得到:
2
4
6
注意:
7
不满足以后就停止了。
后面的:
8
10
虽然也是偶数,也不会再取。
15.2 takeWhile 与 filter 的区别
filter():
2 4 6 7 8 10
↓
所有偶数
↓
2 4 6 8 10
takeWhile():
2 4 6 7 8 10
↑
遇到第一个不满足
停止
↓
2 4 6
这是完全不同的语义。
15.3 dropWhile
对于有序 Stream:
dropWhile(predicate)
表示:
从开头连续丢弃满足条件的最长前缀,然后保留后面的内容。
例如:
Stream.of(
2,
4,
6,
7,
8,
10
)
.dropWhile(
number ->
number % 2 == 0
);
得到:
7
8
10
它不是:
删除所有偶数。
删除所有满足条件的元素应该考虑:
filter(...)
十六、中间操作的执行顺序很重要
假设:
stream
.sorted(...)
.limit(3);
含义:
先对全部候选数据排序
↓
再拿前三名
这适合:
Top 3。
但:
stream
.limit(3)
.sorted(...);
含义:
先拿原 Stream 前三个
↓
只对这三个排序
二者完全不同。
再例如:
stream
.filter(...)
.limit(10);
表示:
满足条件的数据中取前 10 条。
而:
stream
.limit(10)
.filter(...);
表示:
原数据前 10 条里面再过滤。
所以:
Stream 的链式代码虽然很短,但操作顺序就是业务语义。
十七、实践应用:星雨笔录文章处理
假设:
List<Article> articles;
Article:
title
category
viewCount
published
需求:
找到已经发布的 Java 文章,按浏览量降序取前 5 篇,只保留标题。
可以构建:
articles.stream()
.filter(
Article::isPublished
)
.filter(
article ->
"Java".equals(
article.getCategory()
)
)
.sorted(
Comparator
.comparingLong(
Article::getViewCount
)
.reversed()
)
.limit(5)
.map(
Article::getTitle
);
完整逻辑:
List<Article>
↓
stream
↓
已发布
↓
Java 分类
↓
浏览量降序
↓
前 5
↓
提取标题
↓
Stream<String>
这就是 Stream Pipeline 在真实业务中的典型形态。
十八、常见问题
18.1 filter 会修改原集合吗?
不会因为调用:
filter()
就把不满足条件的元素从原集合删除。
它返回:
由满足条件的元素组成的新 Stream。
18.2 map 会修改原来的元素吗?
map() 的语义是:
根据映射函数产生新的元素结果。
例如:
Student → String
不是把原集合的数据结构直接改成 String 集合。
18.3 distinct 根据什么去重?
语义上依据:
Object.equals()
判断元素是否相等。
自定义类型必须特别注意自己的对象相等语义。
18.4 sorted() 一定能排序吗?
不是。
无 Comparator 的:
sorted()
要求元素具有可用的自然顺序。
自定义对象通常应该:
- 实现
Comparable - 或直接提供
Comparator
18.5 limit 和 skip 谁先谁后无所谓吗?
错误。
Stream 操作顺序就是业务语义。
18.6 中间操作一定会马上执行吗?
不一定。
Stream 中间操作通常具有惰性。
只有整个 Pipeline 被终结操作消费时,才真正推动数据处理。
18.7 Stream.concat 是普通中间操作吗?
严格说不是。
它是:
Stream.concat(a, b)
静态方法,用于创建一个惰性拼接 Stream。
18.8 peek 可以代替 forEach 做业务吗?
一般不推荐这样理解。
peek() 主要用于:
调试和观察流水线中的元素。
终结阶段真正处理元素将在下一章学习:
forEach()
等终结操作。
十九、练习与验收
19.1 知识问答
- 什么是 Stream 中间操作?
- 中间操作与终结操作最核心的区别是什么?
filter()接收哪个函数式接口?filter()中返回 true 表示什么?map()的作用是什么?map()能否改变 Stream 元素类型?distinct()根据什么语义判断重复?sorted()和sorted(Comparator)有什么区别?limit(3)表示什么?skip(3)表示什么?limit与skip如何组合表达分页?- 哪些中间操作需要记忆其他元素或位置状态?
flatMap()主要解决什么问题?peek()最适合什么场景?takeWhile()与filter()有什么本质区别?Stream.concat()为什么不属于普通实例中间操作?
19.2 代码阅读
分析:
Stream.of(
10,
30,
20,
40
)
.sorted()
.limit(2);
流水线逻辑是什么?
分析:
Stream.of(
10,
30,
20,
40
)
.limit(2)
.sorted();
与上一段得到的业务语义是否相同?
说明原因。
判断:
students.stream()
.map(
Student::getName
);
map() 前后的 Stream 类型分别是什么?
19.3 手写代码
给出:
List<String> names =
List.of(
"张无忌",
"赵敏",
"张三丰",
"张翠山",
"周芷若"
);
完成:
- 筛选姓张。
- 筛选三个字姓名。
- 去重。
- 按字符串自然顺序排序。
- 只取前两个。
- 跳过第一个。
- 将姓名映射为长度。
暂时只写 Stream Pipeline,不要求收集结果。
19.4 Debug
下面代码业务目标是:
所有及格学生中成绩最高的 3 人。
判断:
students.stream()
.limit(3)
.filter(
s ->
s.getScore() >= 60
)
.sorted(
Comparator
.comparingDouble(
Student::getScore
)
.reversed()
);
操作顺序有什么问题?
重新设计正确 Pipeline。
分析:
students.stream()
.map(
Student::getName
)
.filter(
student ->
student.getScore() > 60
);
哪里存在类型错误?
为什么?
19.5 综合训练
假设:
List<Student> students;
Student:
name
age
score
city
要求设计 Stream Pipeline:
- 只保留北京学生。
- 只保留及格学生。
- 按成绩降序。
- 取前 5。
- 映射为姓名。
要求逐行写出每一步之后的类型:
Stream<Student>
...
...
Stream<String>
19.6 本章验收
关闭资料,能够从零写出并解释:
students.stream()
.filter(...)
.distinct()
.sorted(...)
.skip(...)
.limit(...)
.map(...);
并能准确回答:
filter:留谁?
map:变成什么?
distinct:谁算重复?
sorted:按什么顺序?
skip:前面丢多少?
limit:最多留多少?
还必须能够解释:
为什么 sorted().limit(3)
与
limit(3).sorted()
不等价?
如果能够从业务需求直接拆出正确的 Stream 操作顺序,而不是只会背 API 名称,本章才算真正掌握。