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

  1. 为什么 Stream 最终经常需要收集结果?
  2. toArray() 返回什么类型?
  3. 怎样得到 String[]
  4. collect() 为什么属于终结操作?
  5. Collectors.toList() 解决什么问题?
  6. Stream.toList()Collectors.toList() 的重要区别是什么?
  7. Collectors.toSet() 为什么适合去重结果?
  8. Collectors.toMap() 需要哪两个核心映射函数?
  9. 什么情况下 toMap() 会出现重复 key?
  10. mergeFunction 的作用是什么?
  11. toCollection() 解决什么问题?
  12. CollectorCollectors 有什么区别?
  13. 什么是 Mutable Reduction?
  14. List、Set、Map、Array 分别适合什么结果模型?

17.2 代码阅读

分析:

List<String> result =
        Stream.of(
                "Java",
                "MySQL",
                "Spring"
        )
        .toList();

回答:

  1. result 是否可以安全调用 add()
  2. 为什么?

分析:

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;

完成:

  1. 将所有姓名收集到 List。
  2. 将所有城市收集到 Set。
  3. 将所有姓名收集到 String[]
  4. id → Student 建立 Map。
  5. name → score 建立 Map。
  6. 若姓名重复,Map 中保留更高成绩。
  7. 将姓名收集到 LinkedHashSet
  8. 将姓名使用 ", " 拼接成一个 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

完成:

  1. 获取所有已发布文章标题 List<String>
  2. 获取所有文章分类 Set<String>
  3. 建立 id → Article Map。
  4. 建立 title → viewCount Map。
  5. 如果 title 重复,保留浏览量较大的值。
  6. 将全部文章标题收集成 String[]
  7. 把所有分类使用 ", " 拼接为一个 String。
  8. 按 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 还是数组?

做到这一点,本章才算真正掌握。