1. 概述

本教程将深入探讨 Java 16 引入的 Stream::mapMulti 方法。通过简洁的示例代码,我们直观演示其使用方式。重点对比它与 Stream::flatMap 的相似性,并明确在何种场景下 mapMulti 是更优选择

提示:关于 Java Streams 的更多细节,可参考我们的 Java Streams 专题


2. 方法签名

忽略通配符后,mapMulti 的核心签名可简化为:

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

这是一个 Stream 中间操作,核心参数是 BiConsumer 函数式接口的实现:

  • 接收流元素 T
  • (可选)将其转换为类型 R
  • 调用 mapperConsumer::accept 方法

Java 内部实现中mapper 本质是一个实现 Consumer 接口的缓冲区。每次调用 Consumer::accept 时:

  1. 元素被积累到缓冲区
  2. 自动传递到流管道下游

3. 简单实现示例

场景:对整数列表执行以下操作:

List<Integer> numbers = List.of(1, 2, 3, 4, 5);
double percentage = 0.1;

List<Double> result = numbers.stream()
    .<Double>mapMulti((number, consumer) -> {
        if (number % 2 == 0) {
            double newValue = number * (1 + percentage);
            consumer.accept(newValue);
        }
    })
    .collect(Collectors.toList());

关键点解析

  1. BiConsumer 实现中:
    • 先筛选偶数(隐式 filter 作用)
    • 计算 double 值(隐式 map 作用)
    • 调用 consumer.accept() 输出
  2. 类型注解 <Double> 必需,否则编译器无法推断 R 类型

对比传统写法

List<Double> result2 = numbers.stream()
    .filter(n -> n % 2 == 0)
    .map(n -> n * (1 + percentage))
    .collect(Collectors.toList());

优势

  • mapMulti直接,减少中间操作调用
  • 支持命令式编程,元素转换更灵活

原始类型支持

  • mapMultiToDouble / mapMultiToInt / mapMultiToLong
  • 示例:计算转换后 double 值的和
    double sum = numbers.stream()
      .mapMultiToDouble((number, consumer) -> {
          if (number % 2 == 0) {
              consumer.accept(number * (1 + percentage));
          }
      })
      .sum();
    

4. 更真实的示例

数据模型

class Album {
    private String name;
    private List<Artist> artists;
    // 构造器/getter 省略
}

class Artist {
    private String name;
    // 构造器/getter 省略
}

目标:收集所有「艺术家-专辑」名称对

**使用 mapMulti**:

List<Album> albums = ...; // 专辑列表
List<Pair<String, String>> pairs = albums.stream()
    .<Pair<String, String>>mapMulti((album, consumer) -> {
        for (Artist artist : album.getArtists()) {
            consumer.accept(new ImmutablePair<>(
                artist.getName(), 
                album.getName()
            ));
        }
    })
    .collect(Collectors.toList());

**使用 flatMap**:

List<Pair<String, String>> pairs2 = albums.stream()
    .flatMap(album -> album.getArtists().stream()
        .map(artist -> new ImmutablePair<>(
            artist.getName(), 
            album.getName()
        ))
    )
    .collect(Collectors.toList());

本质:两者均实现「一对多」转换,但实现方式不同。


5. 何时用 mapMulti 替代 flatMap

5.1 替换少量元素时

Java 官方文档指出:当每个流元素需替换为少量(可能为零)元素时,mapMulti 可避免 flatMap 为每组结果创建新流的开销。

场景:根据价格阈值筛选艺术家-专辑对

double upperCost = 10.0;

// mapMulti 实现
List<Pair<String, String>> filtered = albums.stream()
    .<Pair<String, String>>mapMulti((album, consumer) -> {
        if (album.getPrice() < upperCost) {
            for (Artist artist : album.getArtists()) {
                consumer.accept(new ImmutablePair<>(
                    artist.getName(), 
                    album.getName()
                ));
            }
        }
    })
    .collect(Collectors.toList());

// flatMap 实现
List<Pair<String, String>> filtered2 = albums.stream()
    .filter(album -> album.getPrice() < upperCost)
    .flatMap(album -> album.getArtists().stream()
        .map(artist -> new ImmutablePair<>(
            artist.getName(), 
            album.getName()
        ))
    )
    .collect(Collectors.toList());

性能优势

  • mapMulti 无需为每个元素创建中间流
  • 命令式实现减少对象分配开销

5.2 生成结果元素更简单时

场景:在 Album 类中实现复杂逻辑,将艺术家-专辑对及关联厂牌传递给消费者

class Album {
    // ... 其他代码
    
    public void artistAlbumPairsToMajorLabels(Consumer<Pair<String, String>> consumer) {
        for (Artist artist : artists) {
            List<String> labels = artist.getMajorLabels();
            if (labels != null && !labels.isEmpty()) {
                String labelsStr = String.join(",", labels);
                consumer.accept(new ImmutablePair<>(
                    artist.getName(), 
                    this.name + " (" + labelsStr + ")"
                ));
            }
        }
    }
}

调用方式

List<Pair<String, String>> labeledPairs = albums.stream()
    .<Pair<String, String>>mapMulti(Album::artistAlbumPairsToMajorLabels)
    .collect(Collectors.toList());

优势

  • 支持复杂逻辑(如递归,见 Java 文档示例
  • flatMap 实现此类逻辑会极其复杂

6. 结论

mapMulti 在以下场景展现明显优势:

  1. 少量元素替换:避免 flatMap 的中间流创建开销
  2. 命令式元素生成:当用 Consumer 传递元素比返回 Stream 更简单时

源码见 GitHub 仓库


原始标题:Guide to mapMulti in Stream API

« 上一篇: 从JSON生成Java类
» 下一篇: Java Weekly, 第403期