1. 引言
设想我们有一个学生列表,并且每个学生都有自己的爱好列表。例如:
List<List<String>> hobbies = List.of(
List.of("游泳", "国际象棋"),
List.of("足球"),
List.of("编程", "阅读", "电影")
);
你的任务:得到一个包含所有爱好的统一列表,以便了解学生总体都对什么感兴趣。直觉上可以尝试使用 map:
List<Stream<String>> streams = hobbies.stream()
.map(list -> list.stream())
.collect(Collectors.toList());
结果是什么?得到的是“流的列表”!也就是 Stream<Stream<String>>。这并不是我们想要的——我们希望得到的是一个单一的 Stream<String>,从而可以直接处理每个爱好。
想象你有一个盒子,里面装着其他放有玩具的盒子。方法 map 只是把每个盒子取出来并把“盒子”交给你(Stream<Stream<String>>)。而你真正想要的是不再翻盒子,能直接看到所有玩具(Stream<String>)。
2. flatMap:如何“展开”嵌套集合
要“把盒子拆开”并得到一个扁平的流,需要使用 flatMap。
它接收一个函数,该函数对每个元素返回一个流,并会立即把所有这些嵌套流“压平”为一个。
flatMap 的语法
Stream<T> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
简单说:flatMap 期望你为每个元素返回一个流(Stream),然后它会把这些都“铺平”为一个大流。
示例:合并所有学生的爱好
List<String> allHobbies = hobbies.stream()
.flatMap(list -> list.stream())
.collect(Collectors.toList());
System.out.println(allHobbies);
// [游泳, 国际象棋, 足球, 编程, 阅读, 电影]
对每个爱好列表,我们调用了 list.stream()(得到某个学生的爱好流)。然后 flatMap 把所有流“压成”一个大流——现在我们看到的是所有的爱好,而不是“爱好的流”。
可视化示意
hobbies.stream()
|
|---> [游泳, 国际象棋] -> stream
|---> [足球] -> stream
|---> [编程, ...] -> stream
|
flatMap: 将所有内容合并为一个 Stream<String>
为什么 map 不能解决这个问题?
如果使用 map,就会得到 Stream<Stream<String>>,这种结构使用起来不方便:例如,你不能直接遍历所有字符串,而是要进行额外的遍历。
3. flatMap 的实际用法示例
把字符串拆分为字符(List<String> → Stream<Character>)
假设你有一个字符串列表,想得到在所有字符串中出现的所有字符的流:
List<String> words = List.of("Java", "Stream");
List<Character> characters = words.stream()
.flatMap(word -> word.chars().mapToObj(ch -> (char) ch))
.collect(Collectors.toList());
System.out.println(characters);
// [J, a, v, a, S, t, r, e, a, m]
说明:
- word.chars() 返回 IntStream(字符代码的流)。
- mapToObj 把代码点转成字符。
- flatMap 把得到的所有字符流合并为一个。
与 Optional 配合:Stream<Optional<T>> → Stream<T>
假设你有一个 Optional 对象的列表,想只得到那些真实存在的值的流:
List<Optional<String>> optionals = List.of(
Optional.of("Java"),
Optional.empty(),
Optional.of("Stream")
);
List<String> present = optionals.stream()
.flatMap(opt -> opt.stream())
.collect(Collectors.toList());
System.out.println(present);
// [Java, Stream]
要点:
Optional<T>(自 Java 9 起)提供了 stream() 方法,它要么返回空流,要么返回包含一个元素的流。flatMap 会把所有非空值合并为一个流。
4. 新方法 mapMulti:什么时候不需要 flatMap
为什么会有 mapMulti?
Java 16 中新增的 mapMulti 与 flatMap 的工作方式相似,但更高效、更灵活。
flatMap 功能很强,但它有个不足:对于每个元素你都必须创建一个新的 Stream,即使你只想返回 0、1 或多个元素。这可能不够高效,尤其当你只是想“展开”元素而不创建中间集合或流时。
mapMulti 是 flatMap 的改进版,它允许你通过 Consumer 直接把需要的值加入结果流,而无需创建中间结构。你不是返回一个流,而是通过传入的 Consumer 直接指定要加入结果流的元素。
mapMulti 的语法
<R> Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper)
对每个元素都会调用函数 mapper,它接收该元素本身以及一个 Consumer,你可以把要产生的新值“塞”进这个 Consumer。
示例:一次遍历同时完成过滤与展开
假设有一个数字列表,想对偶数在流中添加两次,而对奇数一次也不添加(也就是同时进行过滤与“展开”):
List<Integer> numbers = List.of(1, 2, 3, 4);
List<Integer> result = numbers.stream()
.mapMulti((number, consumer) -> {
if (number % 2 == 0) {
consumer.accept(number);
consumer.accept(number); // 添加两次
}
// 对奇数不做任何事(过滤)
})
.collect(Collectors.toList());
System.out.println(result);
// [2, 2, 4, 4]
与 flatMap 的对比
用 flatMap 实现同样的需求会这样写:
List<Integer> result = numbers.stream()
.flatMap(number -> number % 2 == 0
? Stream.of(number, number)
: Stream.empty())
.collect(Collectors.toList());
在这里,我们仍然被迫为每个元素创建 Stream.of(...) 或 Stream.empty(),即使这样并不高效。
5. 何时使用 flatMap,何时使用 mapMulti?
- map —— 当你把每个元素转换为一个元素时。
- flatMap —— 当你把每个元素转换为一个由多个元素组成的流时。
- mapMulti —— 当你希望生成多个元素但不创建中间流时(更高效,尤其在热点循环中)。
再举一例:将 Map<Integer, List<String>> 展开为 Stream<Pair(Integer, String)>
假设我们有一张映射:id → 爱好列表。
Map<Integer, List<String>> studentHobbies = Map.of(
1, List.of("游泳", "国际象棋"),
2, List.of("足球"),
3, List.of("编程", "阅读", "电影")
);
List<String> allHobbies = studentHobbies.values().stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println(allHobbies);
// [游泳, 国际象棋, 足球, 编程, 阅读, 电影]
如果我们想得到配对(id, 爱好):
List<String> pairs = studentHobbies.entrySet().stream()
.flatMap(entry -> entry.getValue().stream()
.map(hobby -> entry.getKey() + ": " + hobby))
.collect(Collectors.toList());
System.out.println(pairs);
// [1: 游泳, 1: 国际象棋, 2: 足球, 3: 编程, 3: 阅读, 3: 电影]
通过 mapMulti(Java 16+):
List<String> pairs = studentHobbies.entrySet().stream()
.mapMulti((entry, consumer) -> {
for (String hobby : entry.getValue()) {
consumer.accept(entry.getKey() + ": " + hobby);
}
})
.collect(Collectors.toList());
优势:
mapMulti 不会创建中间流,而是把值直接“推入”最终流。
可视化:flatMap 与 mapMulti
| 方法 | 函数返回什么 | 如何合并 | 中间集合? |
|---|---|---|---|
|
一个元素 | 直接 | 否 |
|
流(Stream) | 合并 | 是(中间 Stream) |
|
Consumer(0..n 次) | 合并 | 否(直接添加) |
6. 使用 flatMap 和 mapMulti 的常见错误
错误 1:得到 Stream<Stream<T>> 而不是 Stream<T>。 很多同学在处理“集合的集合”时使用 map 而不是 flatMap。结果不得不写多余的循环。
错误 2:返回值类型不正确。 对于 flatMap,函数必须返回 Stream,而不是 List 或数组。
错误 3:效率问题。 在简单场景下 flatMap 很好用,但如果你对每个元素都要创建 Stream.of 或 Stream.empty(),就可能显得多余。对于这类任务最好使用 mapMulti。
错误 4:mapMulti 在旧版 Java 中不可用。 mapMulti 仅在 Java 16 提供。如果你的 JDK 更旧,该方法将不可用。
错误 5:与 null 相关的问题。 不要从 flatMap 返回 null —— “空”情况一律返回 Stream.empty()。
错误 6:中间集合。 如果可以通过 mapMulti 中的 consumer 直接添加元素,就不要创建多余的列表或流。
GO TO FULL VERSION