Stream 收集结果 | JavaSE
Stream 收集结果
一、学习目标
完成本章后,你应该能够:
- 能够解释为什么 Stream 处理完成后经常需要“收集结果”。
- 能够使用
toArray()收集为数组。 - 能够使用数组构造器引用获得指定类型数组。
- 能够使用
collect(Collectors.toList())收集为 List。 - 能够使用
collect(Collectors.toSet())收集为 Set。 - 能够使用
Collectors.toMap()收集为 Map。 - 能够处理
toMap()中的重复键冲突。 - 能够理解
Stream.toList()与Collectors.toList()的重要差异。 - 能够使用
Collectors.toCollection()指定具体集合实现。 - 能够理解
collect()属于 Mutable Reduction(可变归约)。 - 能够根据业务目标选择 List、Set、Map 或数组。
二、为什么需要收集结果
假设:
Stream<String> stream =
students.stream()
.filter(
student ->
student.getScore() >= 60
)
.map(
Student::getName
);
现在得到:
Stream<String>
但真实业务往往需要:
保存到变量
返回给 Controller
缓存结果
继续使用集合 API
作为方法返回值
转换成 Map
转换成数组
Stream 本身代表:
数据处理流水线。
它不是应该被长期保存和重复访问的数据容器。
所以数据处理完成之后,经常需要:
Stream
↓
收集
↓
List / Set / Map / Array
这就是:
结果收集。
三、toArray:收集到数组
3.1 无参数 toArray
原课程提供:
Object[] toArray()
例如:
Object[] array =
names.stream()
.filter(
name ->
name.startsWith("张")
)
.toArray();
注意结果类型:
Object[]
而不是:
String[]
3.2 为什么是 Object[]
Stream<T> 的无参:
toArray()
无法仅依靠泛型在运行时直接创建:
T[]
因此返回:
Object[]
这在 Java 泛型与数组体系中非常常见。
3.3 获取 String[]
Java 提供:
toArray(IntFunction<A[]> generator)
可以写:
String[] array =
names.stream()
.filter(
name ->
name.startsWith("张")
)
.toArray(
String[]::new
);
这里:
String[]::new
正是前面学习过的:
数组构造器引用。
可以理解为:
Stream 告诉 generator:
“我最终需要长度 n 的数组”
String[]::new
↓
创建 String[n]
3.4 对象数组
例如:
Student[] array =
students.stream()
.filter(
student ->
student.getScore() >= 60
)
.toArray(
Student[]::new
);
这比:
Object[]
类型更加准确。
四、收集到 List
4.1 原课程写法
原始课程使用:
collect(
Collectors.toList()
)
例如:
List<String> names =
students.stream()
.filter(
student ->
student.getScore() >= 60
)
.map(
Student::getName
)
.collect(
Collectors.toList()
);
需要:
import java.util.stream.Collectors;
4.2 整体过程
List<Student>
↓
stream
↓
filter
↓
Stream<Student>
↓
map
↓
Stream<String>
↓
collect(toList())
↓
List<String>
这里真正体现了:
输入集合类型和最终集合元素类型可以完全不同。
五、Java 21 的 Stream.toList()
由于本教程基线是 JDK 21,还需要掌握现代写法:
List<String> names =
students.stream()
.filter(
student ->
student.getScore() >= 60
)
.map(
Student::getName
)
.toList();
比:
.collect(
Collectors.toList()
)
更加简洁。
5.1 两者不能简单认为完全等价
这是 JDK 21 中非常重要的细节。
stream.toList()
返回的 List:
不可修改(unmodifiable)。
例如:
List<String> result =
names.stream()
.toList();
result.add("Java");
会抛出:
UnsupportedOperationException
5.2 Collectors.toList()
而:
Collectors.toList()
官方并不保证:
- 具体实现类;
- 可修改性;
- 可序列化性;
- 线程安全性。
所以不要简单写:
"
Collectors.toList()一定返回 ArrayList。"
官方没有这种保证。
5.3 如果明确需要可修改 ArrayList
可以:
List<String> result =
names.stream()
.collect(
Collectors.toCollection(
ArrayList::new
)
);
此时:
result.add("Java");
业务语义更加明确。
所以可以建立:
只需要最终只读 List
→ Stream.toList()
普通 Collector 风格
→ Collectors.toList()
明确需要 ArrayList
→ Collectors.toCollection(ArrayList::new)
六、收集到 Set
6.1 基本使用
原课程:
collect(
Collectors.toSet()
)
例如:
Set<String> cities =
students.stream()
.map(
Student::getCity
)
.collect(
Collectors.toSet()
);
业务:
获取学生所在的所有不同城市。
6.2 为什么 Set 可以去重
假设:
北京
上海
北京
太原
上海
收集到 Set:
北京
上海
太原
所以当业务结果天然要求:
不重复。
Set 是合理结果类型。
6.3 不要假设具体是 HashSet
Collectors.toSet() 官方并不保证返回:
HashSet
也不保证:
- 具体实现类;
- 可修改性;
- 顺序;
- 线程安全性。
所以:
Set<String> result = ...
应该针对:
Set
接口编程,而不是依赖某个具体实现。
6.4 如果明确需要 TreeSet
例如最终希望:
自动排序 + 去重。
可以:
TreeSet<String> result =
names.stream()
.collect(
Collectors.toCollection(
TreeSet::new
)
);
6.5 LinkedHashSet
如果明确需要:
去重并保持插入 / encounter order。
可以:
LinkedHashSet<String> result =
names.stream()
.collect(
Collectors.toCollection(
LinkedHashSet::new
)
);
这说明:
toCollection()是需要控制具体集合类型时非常重要的 API。
七、收集到 Map
7.1 基本需求
假设:
List<Teacher> teachers;
希望:
教师姓名
↓
教师工资
形成:
Map<String, Double>
可以:
Map<String, Double> salaryMap =
teachers.stream()
.collect(
Collectors.toMap(
Teacher::getName,
Teacher::getSalary
)
);
7.2 toMap 的两个核心函数
Collectors.toMap(
keyMapper,
valueMapper
)
需要回答两个问题:
每个元素的 key 是什么?
每个元素的 value 是什么?
例如:
Teacher::getName
表示:
Teacher → String key
而:
Teacher::getSalary
表示:
Teacher → Double value
所以:
Teacher
↓
name → key
salary → value
↓
Map<String, Double>
八、toMap 的重复键问题
8.1 为什么会冲突
假设:
Teacher A
name = 张三
salary = 5000
Teacher B
name = 张三
salary = 8000
执行:
Collectors.toMap(
Teacher::getName,
Teacher::getSalary
)
两个 Teacher 都产生:
key = 张三
但 Map 中:
一个 key 不能同时直接对应两个不同 value。
因此需要决定:
冲突时保留谁?
8.2 两参数 toMap
如果使用:
Collectors.toMap(
Teacher::getName,
Teacher::getSalary
)
遇到重复 key:
会抛出异常。
所以:
keyMapper
必须确保 key 唯一,或者使用带 mergeFunction 的重载。
8.3 mergeFunction
例如保留工资更高的:
Map<String, Double> salaryMap =
teachers.stream()
.collect(
Collectors.toMap(
Teacher::getName,
Teacher::getSalary,
Math::max
)
);
这里:
Math::max
表示:
旧 value
新 value
↓
选更大的
8.4 保留旧值
(oldValue, newValue) ->
oldValue
例如:
Collectors.toMap(
Teacher::getName,
Teacher::getSalary,
(oldValue, newValue) ->
oldValue
)
8.5 保留新值
(oldValue, newValue) ->
newValue
所以重复 key 不是:
“toMap 很坑”。
而是:
从一组元素建立 Map 时,你必须明确 key 唯一性与冲突策略。
九、指定 Map 实现
toMap() 还有更完整的重载,可以指定最终 Map 类型。
例如:
Map<String, Double> salaryMap =
teachers.stream()
.collect(
Collectors.toMap(
Teacher::getName,
Teacher::getSalary,
(oldValue, newValue) ->
newValue,
LinkedHashMap::new
)
);
四个参数分别可以理解成:
keyMapper
valueMapper
mergeFunction
mapFactory
即:
key 怎么来?
value 怎么来?
key 冲突怎么办?
最终创建什么 Map?
十、collect 到底是什么
10.1 Mutable Reduction
Java 官方将:
collect()
称为:
Mutable Reduction(可变归约)。
普通:
reduce()
更像:
多个值
↓
组合
↓
一个值
例如:
1 + 2 + 3 + 4
而 collect:
元素 1
↓
放进容器
元素 2
↓
继续放进同一个结果容器
元素 3
↓
继续累积
最终:
List / Set / Map
10.2 一个概念模型
假设:
A
B
C
收集到 ArrayList:
创建 []
↓
加入 A
[A]
↓
加入 B
[A, B]
↓
加入 C
[A, B, C]
这就是:
更新可变结果容器的状态。
所以它叫:
Mutable Reduction。
十一、Collector 与 Collectors
这两个单词非常容易混。
Collector
java.util.stream.Collector
是:
描述“怎样收集”的接口 / 抽象。
Collectors
java.util.stream.Collectors
是:
JDK 提供的一组预定义 Collector 工具。
例如:
Collectors.toList()
Collectors.toSet()
Collectors.toMap(...)
Collectors.toCollection(...)
可以记:
Collector
↓
收集策略本身
Collectors
↓
生产常用 Collector 的工具类
十二、收集结果应该选什么类型
List
业务需要:
保留多个结果
允许重复
通常保留 encounter order
选择:
List
Set
业务需要:
去重
不强调重复元素
选择:
Set
Map
业务需要:
key → value
快速按 key 表达数据
选择:
Map
Array
业务 / API 明确需要:
数组
选择:
toArray()
核心原则
不要为了练 Stream 写:
所有结果都 collect(toList())
应该先问:
最终业务数据模型是什么?
再选择结果类型。
十三、完整案例
假设:
List<Student> students;
需求:
找出已经及格的学生,按成绩降序,只保留姓名。
收集 List
List<String> names =
students.stream()
.filter(
student ->
student.getScore() >= 60
)
.sorted(
Comparator
.comparingDouble(
Student::getScore
)
.reversed()
)
.map(
Student::getName
)
.toList();
收集 Set
Set<String> cities =
students.stream()
.filter(
student ->
student.getScore() >= 60
)
.map(
Student::getCity
)
.collect(
Collectors.toSet()
);
收集 Map
假设学号唯一:
Map<Long, Student> studentMap =
students.stream()
.collect(
Collectors.toMap(
Student::getId,
student ->
student
)
);
也可以使用:
Function.identity()
写成:
Map<Long, Student> studentMap =
students.stream()
.collect(
Collectors.toMap(
Student::getId,
Function.identity()
)
);
表示:
value 就是当前 Student 本身。
收集数组
String[] names =
students.stream()
.map(
Student::getName
)
.toArray(
String[]::new
);
十四、JDK 21 实用补充:joining
当 Stream 中是字符串:
Stream<String>
可以:
String result =
names.stream()
.collect(
Collectors.joining(
", "
)
);
例如:
Java, MySQL, Spring
也可以:
Collectors.joining(
", ",
"[",
"]"
)
得到:
[Java, MySQL, Spring]
这是:
把多个字符串收集成一个字符串。
属于 Collector 的典型应用。
十五、JDK 21 实用补充:groupingBy
假设:
List<Student> students;
希望:
城市
↓
该城市学生列表
可以:
Map<String, List<Student>>
studentsByCity =
students.stream()
.collect(
Collectors.groupingBy(
Student::getCity
)
);
结果概念:
北京
→ [Student1, Student2]
上海
→ [Student3]
太原
→ [Student4, Student5]
这类操作以后在真实 Java 业务开发中非常常见。
本章只建立:
groupingBy()可以根据分类函数进行分组。
复杂多级分组暂时不展开。
十六、常见问题
16.1 Stream.toList 和 Collectors.toList 完全一样吗?
不是。
最重要差异:
stream.toList()
JDK 21 明确返回:
unmodifiable List。
而:
Collectors.toList()
不保证具体实现与可修改性。
16.2 Collectors.toList 一定是 ArrayList 吗?
不能这样依赖。
官方没有这种保证。
如果必须要:
ArrayList
明确:
Collectors.toCollection(
ArrayList::new
)
16.3 Collectors.toSet 一定返回 HashSet 吗?
同样不能这样假设。
如果具体实现很重要:
Collectors.toCollection(...)
16.4 toArray() 为什么不是 String[]?
无参:
toArray()
返回:
Object[]
需要 String[]:
toArray(
String[]::new
)
16.5 toMap 为什么可能报错?
非常常见原因:
多个元素映射出了相同 key。
需要:
- 保证 key 唯一;
- 或提供 mergeFunction。
16.6 收集成 Set 等同于 distinct 吗?
最终都可能实现“结果不重复”。
但语义层次不同:
distinct()
↓
Stream Pipeline 中间阶段去重
collect(toSet())
↓
最终结果容器本身不允许重复
例如后面还需要继续 Stream 操作:
stream
.distinct()
.sorted()
.limit(...)
此时应该用:
distinct()
而不是提前结束 Stream 收集成 Set。
16.7 collect 是中间操作吗?
不是。
collect() 是:
终结操作。
调用以后 Stream Pipeline 结束。
本章单独讲它,是因为其结果收集能力本身值得独立成章。
16.8 可以先 collect 再继续 Stream 吗?
例如:
List<String> list =
stream.collect(
Collectors.toList()
);
原 Stream 已经终结。
如果还需要继续:
list.stream()
重新建立新的 Stream。
十七、练习与验收
17.1 知识问答
- 为什么 Stream 最终经常需要收集结果?
toArray()返回什么类型?- 怎样得到
String[]? collect()为什么属于终结操作?Collectors.toList()解决什么问题?Stream.toList()与Collectors.toList()的重要区别是什么?Collectors.toSet()为什么适合去重结果?Collectors.toMap()需要哪两个核心映射函数?- 什么情况下
toMap()会出现重复 key? - mergeFunction 的作用是什么?
toCollection()解决什么问题?Collector与Collectors有什么区别?- 什么是 Mutable Reduction?
- List、Set、Map、Array 分别适合什么结果模型?
17.2 代码阅读
分析:
List<String> result =
Stream.of(
"Java",
"MySQL",
"Spring"
)
.toList();
回答:
result是否可以安全调用add()?- 为什么?
分析:
Map<String, Integer> map =
students.stream()
.collect(
Collectors.toMap(
Student::getName,
Student::getAge
)
);
如果存在两个同名学生,会发生什么?
分析:
String[] names =
students.stream()
.map(
Student::getName
)
.toArray(
String[]::new
);
解释:
String[]::new
在这里承担什么职责。
17.3 手写代码
针对:
List<Student> students;
完成:
- 将所有姓名收集到 List。
- 将所有城市收集到 Set。
- 将所有姓名收集到
String[]。 - 以
id → Student建立 Map。 - 以
name → score建立 Map。 - 若姓名重复,Map 中保留更高成绩。
- 将姓名收集到
LinkedHashSet。 - 将姓名使用
", "拼接成一个 String。
17.4 Debug
下面代码为什么可能出错?
Map<String, Student> map =
students.stream()
.collect(
Collectors.toMap(
Student::getName,
Function.identity()
)
);
给出合理修复。
下面代码有什么风险?
List<String> names =
students.stream()
.map(
Student::getName
)
.toList();
names.add("新学生");
说明原因。
17.5 综合训练
假设星雨笔录存在:
List<Article> articles;
Article:
id
title
category
viewCount
published
完成:
- 获取所有已发布文章标题
List<String>。 - 获取所有文章分类
Set<String>。 - 建立
id → ArticleMap。 - 建立
title → viewCountMap。 - 如果 title 重复,保留浏览量较大的值。
- 将全部文章标题收集成
String[]。 - 把所有分类使用
", "拼接为一个 String。 - 按 category 将文章进行分组。
要求每题说明:
中间操作
↓
最终元素类型
↓
Collector / toArray
↓
最终结果类型
17.6 本章验收
闭卷画出:
Stream<T>
│
├── toArray()
│ ↓
│ Object[]
│
├── toArray(T[]::new)
│ ↓
│ T[]
│
├── toList()
│ ↓
│ List<T>
│ JDK 21:unmodifiable
│
└── collect(...)
│
├── toList
├── toSet
├── toMap
└── toCollection
并能够完整解释:
为什么 collect 是 Mutable Reduction?
为什么 toMap 要考虑重复 key?
为什么 Stream.toList() 不能简单等价成
Collectors.toList()?
为什么业务目标决定最终应该选择
List、Set、Map 还是数组?
做到这一点,本章才算真正掌握。