何时以及如何执行一到0..n映射流映射在平面地图上多次

2022-09-01 22:37:10

我一直在浏览最新LTE Java 17版本的新闻和源代码,我遇到了名为.抢先体验的JavaDoc表示它类似于.mapMultiflatMap

<R> Stream<R> mapMulti(BiConsumer<? super T,? super Consumer<R>> mapper)
  • 如何使用此方法执行一到0..n的映射?
  • 新方法的工作原理以及它与 有何不同。什么时候每个都更可取?flatMap
  • 可以调用多少次?mapper

答案 1

Stream::mapMulti是一种分类为中间操作的新方法。

它需要将要处理的元素的 a。后者使该方法乍一看很奇怪,因为它与我们在其他中间方法(如 、或 )中所习惯的方法不同,或者它们都不使用 的任何变体。BiConsumer<T, Consumer<R>> mapperConsumermapfilterpeek*Consumer

API 本身在 lambda 表达式中提供的权利的目的是接受在后续管道中可用的任何数字元素。因此,所有元素,无论有多少,都将被传播。Consumer

使用简单代码段的解释

  • 一对一(0..1)映射(类似于filter)

    仅对少数选定项使用 可实现类似筛选器的管道。如果根据谓词检查元素并将其映射到不同的值,这可能会很有用,否则将使用 和 的组合来完成。以下consumer.accept(R r)filtermap

    Stream.of("Java", "Python", "JavaScript", "C#", "Ruby")
          .mapMulti((str, consumer) -> {
              if (str.length() > 4) {
                  consumer.accept(str.length());  // lengths larger than 4
              }
          })
          .forEach(i -> System.out.print(i + " "));
    
    // 6 10
    
  • 一对一映射(类似于map)

    使用前面的示例,当省略条件并且每个元素都映射到一个新元素并使用 接受时,该方法实际上表现得像:consumermap

    Stream.of("Java", "Python", "JavaScript", "C#", "Ruby")
          .mapMulti((str, consumer) -> consumer.accept(str.length()))
          .forEach(i -> System.out.print(i + " "));
    
    // 4 6 10 2 4
    
  • 一对多映射(类似于flatMap)

    在这里,事情变得有趣,因为人们可以拨打任意次数。假设我们想要复制表示字符串长度的数字本身,即 成为。 成为。并变得一无所有。consumer.accept(R r)222444440

    Stream.of("Java", "Python", "JavaScript", "C#", "Ruby", "")
          .mapMulti((str, consumer) -> {
              for (int i = 0; i < str.length(); i++) {
                  consumer.accept(str.length());
              }
          })
          .forEach(i -> System.out.print(i + " "));
    
    // 4 4 4 4 6 6 6 6 6 6 10 10 10 10 10 10 10 10 10 10 2 2 4 4 4 4 
    
    

与平面地图的比较

这种机制的理念是可以多次调用(包括零),并且它在内部的使用允许将元素推送到单个扁平化的Stream实例中,而无需为每组输出元素创建一个新元素,这与.JavaDoc 指出了两个用例,当使用此方法优于以下方法时:SpinedBufferflatMapflatMap

  • 将每个流元素替换为少量(可能为零)元素时。使用此方法可避免根据 flatMap 的要求为每组结果元素创建新的 Stream 实例的开销。
  • 当使用命令式方法生成结果元素比以 Stream 的形式返回结果元素更容易时。

在性能方面,在这种情况下,新方法是赢家。查看此答案底部的基准测试。mapMulti

筛选器映射方案

使用此方法代替或单独使用此方法没有意义,因为它很长,而且无论如何都会创建一个中间流。例外情况可能是替换一起调用的链,这在检查元素类型及其强制转换等情况下会很方便。filtermap.filter(..).map(..)

int sum = Stream.of(1, 2.0, 3.0, 4F, 5, 6L)
                .mapMultiToInt((number, consumer) -> {
                    if (number instanceof Integer) {
                        consumer.accept((Integer) number);
                    }
                })
                .sum();
// 6
int sum = Stream.of(1, 2.0, 3.0, 4F, 5, 6L)
                .filter(number -> number instanceof Integer)
                .mapToInt(number -> (Integer) number)
                .sum();

如上所示,它的变化,如mapMultiToDoublemapMultiToIntmapMultiToLong被引入。这伴随着原始流中的方法,例如IntStream mapMulti(IntStream.IntMapMultiConsumer mapper)。此外,还引入了三个新的功能接口。基本上,它们是 的原始变体,例如:mapMultiBiConsumer<T, Consumer<R>>

@FunctionalInterface
interface IntMapMultiConsumer {
    void accept(int value, IntConsumer ic);
}

组合的真实用例场景

此方法的真正功能在于其使用灵活性,并且一次只能创建一个Stream,这是与.以下两个代码段表示由类表示的商品的平面映射,并基于特定条件(产品类别和变体可用性)。flatMapProductList<Variation>0..nOffer

  • Product与 、 和 。String nameint basePriceString categoryList<Variation> variations
  • Variation与 和 。String nameint priceboolean availability
List<Product> products = ...
List<Offer> offers = products.stream()
        .mapMulti((product, consumer) -> {
            if ("PRODUCT_CATEGORY".equals(product.getCategory())) {
                for (Variation v : product.getVariations()) {
                    if (v.isAvailable()) {
                        Offer offer = new Offer(
                            product.getName() + "_" + v.getName(),
                            product.getBasePrice() + v.getPrice());
                        consumer.accept(offer);
                    }
                }
            }
        })
        .collect(Collectors.toList());
List<Product> products = ...
List<Offer> offers = products.stream()
        .filter(product -> "PRODUCT_CATEGORY".equals(product.getCategory()))
        .flatMap(product -> product.getVariations().stream()
            .filter(Variation::isAvailable)
            .map(v -> new Offer(
                product.getName() + "_" + v.getName(),
                product.getBasePrice() + v.getPrice()
            ))
        )
        .collect(Collectors.toList());

与以前版本的 Stream 方法组合的声明性方法相比,使用 更倾向于使用 、 和 。从这个角度来看,这取决于用例是否更容易使用命令式方法。递归是 JavaDoc 中描述的一个很好的例子。mapMultiflatMapmapfilter

基准

正如承诺的那样,我从评论中收集的想法中写了一堆微观基准。只要有相当多的代码要发布,我就创建了一个包含实现细节的GitHub存储库,我将只共享结果。

流::平面地图(函数) vs 流::地图多(双消费者) 来源

在这里,我们可以看到巨大的差异和证明,较新的方法实际上按所述工作,并且它的使用避免了为每个处理的元素创建新的Stream实例的开销。

Benchmark                                   Mode  Cnt   Score   Error  Units
MapMulti_FlatMap.flatMap                    avgt   25  73.852 ± 3.433  ns/op
MapMulti_FlatMap.mapMulti                   avgt   25  17.495 ± 0.476  ns/op

Stream::filter(Predicate).map(Function) vs Stream::mapMulti(BiConsumer) Source

使用链接管道(不是嵌套的)是可以的。

Benchmark                                   Mode  Cnt    Score  Error  Units
MapMulti_FilterMap.filterMap                avgt   25   7.973 ± 0.378  ns/op
MapMulti_FilterMap.mapMulti                 avgt   25   7.765 ± 0.633  ns/op 

Stream::flatMap(Function) with Optional::stream() vs Stream::mapMulti(BiConsumer) Source

这个非常有趣,特别是在用法方面(参见源代码):我们现在能够使用扁平化,并且正如预期的那样,在这种情况下,新方法会更快一些。mapMulti(Optional::ifPresent)

Benchmark                                   Mode  Cnt   Score   Error  Units
MapMulti_FlatMap_Optional.flatMap           avgt   25  20.186 ± 1.305  ns/op
MapMulti_FlatMap_Optional.mapMulti          avgt   25  10.498 ± 0.403  ns/op

答案 2

解决此方案

当使用命令式方法生成结果元素比以 Stream 的形式返回结果元素更容易时。

我们可以看到它现在具有 yield 语句 C# 的有限变体。限制在于,我们总是需要来自流的初始输入,因为这是一个中间操作,此外,我们在一个函数评估中推送的元素没有短路。

尽管如此,它还是开辟了有趣的机会。

例如,实现斐波那契数列流以前需要使用能够保存两个值的临时对象的解决方案

现在,我们可以像这样使用:

IntStream.of(0)
    .mapMulti((a,c) -> {
        for(int b = 1; a >=0; b = a + (a = b))
            c.accept(a);
    })
    /* additional stream operations here */
    .forEach(System.out::println);

当值溢出时,它会停止,如前所述,当我们使用不消耗所有值的终端操作时,它不会短路,但是,生成当时忽略的值的循环可能仍然比其他方法更快。int

另一个受此答案启发的示例,从根到最具体的类层次结构进行迭代:

Stream.of(LinkedHashMap.class).mapMulti(MapMultiExamples::hierarchy)
    /* additional stream operations here */
    .forEach(System.out::println);
}
static void hierarchy(Class<?> cl, Consumer<? super Class<?>> co) {
    if(cl != null) {
        hierarchy(cl.getSuperclass(), co);
        co.accept(cl);
    }
}

与旧方法不同,它不需要额外的堆存储,并且可能会运行得更快(假设合理的类深度不会使递归适得其反)。

还有像这样的怪物

List<A> list = IntStream.range(0, r_i).boxed()
    .flatMap(i -> IntStream.range(0, r_j).boxed()
        .flatMap(j -> IntStream.range(0, r_k)
            .mapToObj(k -> new A(i, j, k))))
    .collect(Collectors.toList());

现在可以写成

List<A> list = IntStream.range(0, r_i).boxed()
    .<A>mapMulti((i,c) -> {
        for(int j = 0; j < r_j; j++) {
            for(int k = 0; k < r_k; k++) {
                c.accept(new A(i, j, k));
            }
        }
    })
    .collect(Collectors.toList());

与嵌套步骤相比,它失去了一些并行性机会,而参考实现无论如何都没有利用这些机会。对于像上面这样的非短路操作,新方法可能会受益于减少的装箱和更少的捕获 lambda 表达式的实例化。但是,当然,它应该被明智地使用,而不是将每个构造重写为命令式版本(在这么多人试图将每个命令式代码重写为函数式版本之后)......flatMap


推荐