ZetCode

Java Stream: flatMap vs mapMulti

最后修改时间:2025 年 6 月 8 日

在使用 Java Streams 时,开发人员经常需要转换和扩展流元素。Java 提供了两种主要方法来实现此目的:flatMap(自 Java 8 起)和 mapMulti(Java 16 中引入)。本教程探讨了它们的区别、性能特征和理想用例。

虽然这两种方法都可以将每个流元素转换为零个或多个输出元素,但它们遵循不同的范例。flatMap 使用一种函数式方法,需要新的 Stream 实例,而 mapMulti 使用一种命令式、基于消费者的的方法,在某些情况下可能更有效。

关键差异概述

下表总结了 Java Streams 中 mapMultiflatMap 之间的关键差异。

功能 mapMulti flatMap
引入版本 Java 16 Java 8
编程风格 命令式(推送) 函数式(拉取)
性能 更适合简单的扩展 更适合复杂的转换
中间对象 没有中间流 创建中间流
可读性 更适合命令式逻辑 更适合函数式管道

这两种方法都可以实现类似的结果,但它们的性能和可读性会根据具体用例而有很大差异。了解这些差异有助于您为流处理需求选择正确的工具。

基本元素扩展

此示例展示了这两种方法如何将每个输入元素扩展为多个输出元素。我们将字符串转换为它们的大写和小写变体。

FlatMapExample.java
void main() {

    List<String> words = List.of("apple", "banana", "cherry");
    
    List<String> result = words.stream()
        .flatMap(word -> Stream.of(word.toUpperCase(), word.toLowerCase()))
        .toList();
        
    System.out.println(result);
}

此代码使用 flatMap 将每个单词转换为两个元素:它的大写和小写版本。flatMap 方法接受一个函数,该函数为每个输入元素返回一个 Stream,然后将其扁平化为单个输出流。

mapMulti 方法使用 BiConsumer 为每个输入发出多个元素。consumer 直接接受转换后的元素,避免了创建中间 Stream 实例的需要。

MapMultiExample.java
void main() {

    List<String> words = List.of("apple", "banana", "cherry");
    
    List<String> result = words.stream()
            .<String>mapMulti((word, consumer) -> {
                consumer.accept(word.toUpperCase());
                consumer.accept(word.toLowerCase());
            })
            .toList();
        
    System.out.println(result);
}

两个版本产生相同的输出,但 mapMulti 避免了创建中间 Stream 实例。对于像这样的简单扩展,mapMulti 通常表现出更好的性能,同时保持可读性。

条件元素扩展

当您需要根据某些条件有条件地扩展元素时,两种方法之间的差异会变得更加明显。

FlatMapConditional.java
void main() {

    List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
    
    List<Integer> result = numbers.stream()
        .flatMap(n -> {
            if (n % 2 == 0) {
                return Stream.of(n, n * 10);
            }
            return Stream.empty();
        })
        .toList();
        
    System.out.println(result);
}

此代码使用 flatMap 将偶数转换为两个元素:数字本身及其十倍。如果数字是奇数,它将返回一个空 Stream。这种方法可行,但在处理多个条件或复杂逻辑时,可能会导致代码可读性降低。


MapMultiConditional.java
void main() {

    List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
    
    List<Integer> result = numbers.stream()
        .<Integer>mapMulti((n, consumer) -> {
            if (n % 2 == 0) {
                consumer.accept(n);
                consumer.accept(n * 10);
            }
        })
        .toList();
        
    System.out.println(result);
}

mapMulti 版本更自然地处理条件逻辑,而无需显式返回空流。这使得在处理多个条件或复杂的 branching 逻辑时,代码更加简单明了。

何时使用哪种方法

maptMultiflatMap 方法各有优势和理想用例。 了解何时使用每种方法可以帮助您编写更高效、更易读的代码。

在某些情况下,mapMulti 方法比 flatMap 具有多个优势

当您需要执行简单扩展(1 对少数元素)或想要以更命令式的方式处理复杂的条件逻辑时,mapMulti 方法特别有用。它允许您直接发出元素,而无需创建中间 Stream 对象,这可以在某些情况下提高性能。

在以下情况下,首选 mapMulti

在以下情况下,首选 flatMap

真实示例:订单处理

让我们研究一个更实际的例子,处理包含嵌套项的电子商务订单。

OrderProcessingFlatMap.java
record Order(String id, List<Item> items) {}
record Item(String sku, int quantity, double price) {}

void main() {

    List<Order> orders = List.of(
        new Order("O1", List.of(
            new Item("I1", 2, 9.99),
            new Item("I2", 1, 19.99)
        )),
        new Order("O2", List.of(
        new Item("I3", 5, 4.99)
        ))
    );

    List<String> result = orders.stream()
        .flatMap(order -> 
            order.items().stream()
                .filter(item -> item.quantity() > 1)
                .map(item -> "%s: %s x %.2f".formatted(
                    order.id(), item.sku(), item.price() * item.quantity()
                ))
        )
        .toList();
        
    System.out.println(result);
}

此代码使用 flatMap 将每个单词转换为两个元素:它的大写和小写版本。flatMap 方法接受一个函数,该函数为每个输入元素返回一个 Stream,然后将其扁平化为单个输出流。

OrderProcessingMapMulti.java
record Order(String id, List<Item> items) {}
record Item(String sku, int quantity, double price) {}

void main() {

    List<Order> orders = List.of(
        new Order("O1", List.of(
            new Item("I1", 2, 9.99),
            new Item("I2", 1, 19.99)
        )),
        new Order("O2", List.of(
            new Item("I3", 5, 4.99)
        ))
    );
    
    List<String> result = orders.stream()
        .<String>mapMulti((order, consumer) -> {
            for (Item item : order.items()) {
                if (item.quantity() > 1) {
                    String line = "%s: %s x %.2f".formatted(
                        order.id(), item.sku(), item.price() * item.quantity()
                    );
                    consumer.accept(line);
                }
            }
        })
        .toList();
        
    System.out.println(result);
}

在这个真实的场景中,这两种方法都有效。当已经使用返回 Streams 的方法时,flatMap 版本可能更合适,而 mapMulti 在处理复杂条件时提供更好的性能和更直接的命令式逻辑。

迁移技巧

flatMap 迁移到 mapMulti

  1. 将返回 Stream 的函数替换为 BiConsumer
  2. 将元素发射从 return Stream.of(x) 更改为 consumer.accept(x)
  3. 将过滤逻辑移动到发射之前,而不是使用 filter
  4. 对于空结果,只需不调用 consumer

请记住,迁移并非总是必要的 - flatMap 仍然是许多场景的不错选择,尤其是在使用现有的返回 Stream 的方法时。

来源

Java Stream mapMulti 文档
Java Stream flatMap 文档

mapMultiflatMap 都是 Stream API 中的宝贵工具。mapMulti 提供了一种命令式的替代方案,对于某些模式来说可能更有效,而 flatMap 仍然是一种更函数式的方法。 根据您的具体要求、编码风格和性能需求进行选择。

作者

我叫 Jan Bodnar,是一位充满热情的程序员,拥有丰富的编程经验。 我自 2007 年以来一直在撰写编程文章。 迄今为止,我已经撰写了 1,400 多篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有Java教程