CodeGym /课程 /JAVA 25 SELF /flatMap 和 mapMulti 方法

flatMap 和 mapMulti 方法

JAVA 25 SELF
第 32 级 , 课程 0
可用

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 中新增的 mapMultiflatMap 的工作方式相似,但更高效、更灵活。

flatMap 功能很强,但它有个不足:对于每个元素你都必须创建一个新的 Stream,即使你只想返回 0、1 或多个元素。这可能不够高效,尤其当你只是想“展开”元素而不创建中间集合或流时。

mapMultiflatMap 的改进版,它允许你通过 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

方法 函数返回什么 如何合并 中间集合?
map
一个元素 直接
flatMap
流(Stream) 合并 是(中间 Stream)
mapMulti
Consumer(0..n 次) 合并 否(直接添加)

6. 使用 flatMap 和 mapMulti 的常见错误

错误 1:得到 Stream<Stream<T>> 而不是 Stream<T>。 很多同学在处理“集合的集合”时使用 map 而不是 flatMap。结果不得不写多余的循环。

错误 2:返回值类型不正确。 对于 flatMap,函数必须返回 Stream,而不是 List 或数组。

错误 3:效率问题。 在简单场景下 flatMap 很好用,但如果你对每个元素都要创建 Stream.ofStream.empty(),就可能显得多余。对于这类任务最好使用 mapMulti

错误 4:mapMulti 在旧版 Java 中不可用。 mapMulti 仅在 Java 16 提供。如果你的 JDK 更旧,该方法将不可用。

错误 5:与 null 相关的问题。 不要从 flatMap 返回 null —— “空”情况一律返回 Stream.empty()

错误 6:中间集合。 如果可以通过 mapMulti 中的 consumer 直接添加元素,就不要创建多余的列表或流。

1
任务
JAVA 25 SELF, 第 32 级, 课程 0
已锁定
为学校社团收集所有学生的兴趣 📚
为学校社团收集所有学生的兴趣 📚
1
任务
JAVA 25 SELF, 第 32 级, 课程 0
已锁定
为每个学生创建爱好目录 🧑‍🎓
为每个学生创建爱好目录 🧑‍🎓
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION