ZetCode

Java Stream mapMulti

上次修改时间:2025 年 6 月 5 日

mapMulti 方法是在 Java 16 中引入的,作为 Stream API 中 flatMap 的一个更灵活的替代方案。 它允许使用消费者风格的方法将每个流元素转换为零个或多个元素。 当您需要对元素扩展或转换进行命令式控制时,此方法特别有用。

mapMulti 方法是 map 的一个改进版本,旨在将每个流元素转换为多个输出元素,或者完全排除它们。

flatMap 需要为每个输入元素返回一个新的流不同,mapMulti 允许您以命令式方式将元素推送到消费者。 在某些情况下,这可以使代码更具可读性,并通过避免创建中间流来提高性能。

mapMulti 基础

mapMulti 方法接受一个 BiConsumer,它接收每个流元素和一个消费者。 对于每个输入元素,您可以

方法签名是

<R> Stream<R> mapMulti(BiConsumer<? super T,? super Consumer<R>> mapper)

其中 T 是输入类型,R 是输出类型。

简单元素扩展

此示例演示使用 mapMulti 进行基本元素扩展。 我们会将列表中的每个字符串转换为其大写和小写变体。

Main.java
void main() {

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

输出将显示每个单词的大写和小写形式。 此处的 mapMulti 方法比使用 flatMap 更直接,后者需要为每个元素创建一个流。 这表明 mapMulti 如何简化某些扩展场景。

条件元素发射

当您需要对元素转换进行条件逻辑时,mapMulti 表现出色。 此示例根据多个条件过滤和转换数字。

Main.java
void main() {

    Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .mapMulti((num, consumer) -> {
            if (num % 2 == 0) {
                consumer.accept(num * 10);
            }
            if (num % 3 == 0) {
                consumer.accept(num * 100);
            }
        })
        .forEach(System.out::println);
}

此代码根据可除性输出转换后的数字:偶数乘以 10,能被 3 整除的数字乘以 100。某些数字(如 6)将产生两种转换。 与等效的 flatMap 方法相比,命令式风格使条件逻辑更加清晰。

使用 mapMulti 进行类型转换

mapMulti 可以在优雅地处理 null 值的​​同时执行类型转换。 此示例将字符串转换为整数,跳过无效条目。

Main.java
void main() {

    List<String> inputs = new ArrayList<>();
    inputs.add("1");
    inputs.add("2");
    inputs.add("three");
    inputs.add("4");
    inputs.add(null);
    inputs.add("5");

    inputs.stream()
            .mapMulti((str, consumer) -> {
                try {
                    if (str != null) {
                        consumer.accept(Integer.parseInt(str));
                    }
                } catch (NumberFormatException e) {
                    // Skip invalid entries
                }
            })
            .forEach(System.out::println);
}

这可以安全地将有效字符串转换为整数,同时忽略 null 和非数字字符串。 mapMulti 消费者中的 try-catch 块提供了一种干净的方式来处理转换错误,而不会破坏流管道。

扁平化字符串流

mapMulti 方法还可用于将字符串流扁平化为单个单词列表。 当您有一个逗号分隔值的流并想要从这些字符串中提取每个单词时,这特别有用。 以下示例演示了如何使用 mapMulti 将每行拆分为单词并将结果数组扁平化为单个列表来实现此目的。

Main.java
void main() {

    String data = """
            one,two
            falcon,eagle
            spy,hunter
            string,number
            nest,tree
            cup,table
            cloud,rain
            war,artillery
            water,buck
            risk,gain
            """;

    List<String> res = data.lines()
            .map(line -> line.split(",")) // Map each line to an array of words
            .flatMap(Arrays::stream) // Flatten arrays into a single stream
            .toList();

    System.out.println(res);

    var res2 = data.lines().<String>mapMulti((line, consumer) -> {

        for (var c : line.split(",")) {

            consumer.accept(c);
        }

    }).toList();

    System.out.println(res2);

}

在此示例中,我们首先使用 map 将每行拆分为一个单词数组,然后使用 flatMap 将这些数组扁平化为单个流。 mapMulti 方法通过直接迭代拆分的单词并将每个单词传递给消费者来实现相同的结果。

嵌套结构扁平化

mapMulti 非常适合扁平化嵌套数据结构。 此示例提取嵌套列表中的所有元素,同时添加元数据。

Main.java
record NestedItem(int id, List<String> values) {}

void main() {
    
    List<NestedItem> items = List.of(
        new NestedItem(1, List.of("A", "B")),
        new NestedItem(2, List.of("C")),
        new NestedItem(3, List.of("D", "E", "F"))
    );
    
    items.stream()
        .mapMulti((item, consumer) -> {
            for (String value : item.values()) {
                consumer.accept(item.id() + ":" + value);
            }
        })
        .forEach(System.out::println);
}

输出将每个嵌套元素与其父 ID 组合在一起。 与 flatMap 相比,mapMulti 内部的命令式循环可以更好地控制扁平化过程,尤其是在需要组合来自结构中不同级别的数据时。

性能比较

此示例比较了 mapMultiflatMap 的简单扩展操作,展示了潜在的性能优势。

Main.java
void main() {

    int size = 10_000_000;
    
    long mapMultiTime = measureTime(() -> 
        IntStream.range(0, size)
            .mapToObj(i -> i)
            .mapMulti((i, consumer) -> {
                consumer.accept(i * 2);
                consumer.accept(i * 3);
            })
            .count());
    
    long flatMapTime = measureTime(() -> 
        IntStream.range(0, size)
            .mapToObj(i -> i)
            .flatMap(i -> Stream.of(i * 2, i * 3))
            .count());
            
    System.out.println("mapMulti time: " + mapMultiTime + "ms");
    System.out.println("flatMap time: " + flatMapTime + "ms");
}

long measureTime(Runnable operation) {
    long start = System.currentTimeMillis();
    operation.run();
    return System.currentTimeMillis() - start;
}

虽然结果因环境而异,但通过避免创建中间流的开销,mapMulti 通常在简单扩展方面表现出更好的性能。 但是,flatMap 对于复杂的转换可能更具可读性,因此请根据您的具体用例进行选择。

与其他操作结合使用

mapMulti 可以有效地与其他流操作结合使用。 此示例在一个管道中显示了过滤、转换和集合。

Main.java
void main() {

    List<String> phrases = List.of(
            "Java 16", "Stream API", "mapMulti", "method", "examples");

    Map<Integer, List<String>> result = phrases.stream()
            .<String>mapMulti((String phrase, Consumer<String> consumer) -> {
                String[] words = phrase.split(" ");
                for (String word : words) {
                    if (word.length() > 3) {
                        consumer.accept(word.toLowerCase());
                    }
                }
            })
            .collect(Collectors.groupingBy(String::length));

    System.out.println(result);
}

此管道将短语拆分为单词,过滤掉短单词,转换为小写,然后按单词长度分组。 mapMulti 操作在一个步骤中处理拆分和过滤,展示了它如何将多个转换合并为一个操作,同时保持可读性。

实际用例

此示例展示了 mapMulti 的一个实际应用,用于处理具有条件逻辑的层次结构业务数据。

Main.java
record Department(String name, List<Employee> employees) {}
record Employee(String name, int salary, boolean active) {}

void main() {

    List<Department> departments = List.of(
        new Department("Engineering", List.of(
            new Employee("Alice", 90000, true),
            new Employee("Bob", 85000, false)
        )),
        new Department("Marketing", List.of(
            new Employee("Carol", 80000, true)
        ))
    );
    
    departments.stream()
        .mapMulti((dept, consumer) -> {
            if (dept.employees().size() > 1) {
                dept.employees().stream()
                    .filter(Employee::active)
                    .map(e -> dept.name() + " - " + e.name())
                    .forEach(consumer);
            }
        })
        .forEach(System.out::println);
}

这处理了拥有多个员工的部门,过滤了活跃员工,并创建了部门-员工字符串。 mapMulti 方法干净地处理了嵌套条件和转换,同时保持了良好的可读性。 这种模式在处理分层数据的业务应用程序中很常见。

来源

Java Stream mapMulti 文档

mapMulti 方法为 Stream API 提供了有价值的补充,在功能管道中提供命令式风格的控制。 虽然不能完全替代 flatMap,但它在需要复杂条件逻辑或每个元素进行多次转换的场景中表现出色。 根据特定用例的可读性和性能要求在两者之间进行选择。

作者

我叫 Jan Bodnar,是一名充满热情的程序员,拥有丰富的编程经验。 我从 2007 年开始撰写编程文章。到目前为止,我已经撰写了 1,400 多篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有Java教程