Nik*_*las 22 java java-stream flatmap java-16 mapmulti
我一直在浏览 Java 16 的新闻和源代码,并且遇到了名为mapMulti. 早期访问的JavaDoc说它类似于flatMap并且已经被批准用于完全相同的 Java 版本。
<R> Stream<R> mapMulti?(BiConsumer<? super T,?? super Consumer<R>> mapper)
Run Code Online (Sandbox Code Playgroud)
flatMap. 什么时候最好?mapper可以调用多少次?Nik*_*las 32
Stream::mapMulti是一种被归类为中间操作的新方法。
它需要一个BiConsumer<T, Consumer<R>> mapper即将被处理的元素 a Consumer。后者使得该方法看第一眼奇怪,因为它是从我们在其它中间的方法,如使用不同的map,filter或peek在那里他们没有使用任何变化*Consumer。
ConsumerAPI 本身在 lambda 表达式中提供的权限的目的是接受在后续管道中可用的任何数字元素。因此,所有元素,无论有多少,都将被传播。
一对一 (0..1) 映射(类似于filter)
使用consumer.accept(R r)了只有少数选定的项目达到过滤器一样的管道。这可能会在检查对谓词的元素的情况下获得有用的和它映射到一个不同的值,这将使用的组合来完成,否则filter和map代替。下列
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
Run Code Online (Sandbox Code Playgroud)
一对一映射(类似于map)
使用前面的示例,当省略条件并且每个元素都映射到一个新元素并使用 接受时consumer,该方法的有效行为如下map:
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
Run Code Online (Sandbox Code Playgroud)
一对多映射(类似于flatMap)
事情变得有趣了,因为人们可以调用consumer.accept(R r) 任意次数。假设我们想自己复制代表字符串长度的数字,即2变成2, 2。4变成4, 4, 4, 4。并0变得一无所有。
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
Run Code Online (Sandbox Code Playgroud)
这种机制的想法是可以多次调用(包括零),并且它在SpinedBuffer内部的使用允许将元素推送到单个扁平的 Stream 实例中,而无需为每组输出元素创建一个新的与flatMap. 当使用此方法优于使用此方法时,JavaDoc说明了两个用例flatMap:
- 用少量(可能为零)元素替换每个流元素时。使用这种方法可以避免为每组结果元素创建一个新的 Stream 实例的开销,正如 flatMap 所要求的。
- 当使用命令式方法生成结果元素比以流的形式返回它们更容易时。
在性能方面,新方法mapMulti在这种情况下是赢家。查看此答案底部的基准测试。
由于其冗长并且无论如何都会创建一个中间流,因此使用此方法代替filter或map单独使用是没有意义的。例外可能是替换被调用的.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
Run Code Online (Sandbox Code Playgroud)
int sum = Stream.of(1, 2.0, 3.0, 4F, 5, 6L)
.filter(number -> number instanceof Integer)
.mapToInt(number -> (Integer) number)
.sum();
Run Code Online (Sandbox Code Playgroud)
如上所述,它的变体如mapMultiToDouble、mapMultiToInt和mapMultiToLong被引入。这伴随着mapMulti原始流中的方法,例如IntStream mapMulti?(IntStream.IntMapMultiConsumer mapper). 此外,还引入了三个新的功能接口。基本上,它们是 的原始变体BiConsumer<T, Consumer<R>>,例如:
@FunctionalInterface
interface IntMapMultiConsumer {
void accept(int value, IntConsumer ic);
}
Run Code Online (Sandbox Code Playgroud)
这种方法的真正威力在于它的使用灵活性和一次只创建一个 Stream,这是优于flatMap. 两个片段下面表示flatmapping的Product和其List<Variation>成0..n提供表示由所述Offer类和基于某些条件(产品类别和变动情况而定)。
Product有String name,int basePrice,String category和List<Variation> variations。Variation与String name,int price和boolean availability。@FunctionalInterface
interface IntMapMultiConsumer {
void accept(int value, IntConsumer ic);
}
Run Code Online (Sandbox Code Playgroud)
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());
Run Code Online (Sandbox Code Playgroud)
利用mapMulti与使用在后者片断看到的先前版本Stream方法组合的声明方法更势在必行倾斜flatMap,map和filter。从这个角度来看,使用命令式方法是否更容易取决于用例。递归是JavaDoc 中描述的一个很好的例子。
正如承诺的那样,我根据从评论中收集的想法编写了一堆微型基准测试。只要有相当多的代码要发布,我已经创建了一个包含实现细节的GitHub 存储库,我将只分享结果。
Stream::flatMap(Function)VSStream::mapMulti(BiConsumer) 来源
在这里,我们可以看到巨大的差异,并证明了较新的方法实际上按照描述的方式工作,并且它的使用避免了为每个处理的元素创建一个新的 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
Run Code Online (Sandbox Code Playgroud)
Stream::filter(Predicate).map(Function)VSStream::mapMulti(BiConsumer) 来源
使用链式管道(虽然不是嵌套的)很好。
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
Run Code Online (Sandbox Code Playgroud)
Stream::flatMap(Function)与Optional::stream()VSStream::mapMulti(BiConsumer) 源
这个非常有趣,尤其是在使用方面(参见源代码):我们现在可以使用扁平化,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
Run Code Online (Sandbox Code Playgroud)
Hol*_*ger 11
解决方案
当使用命令式方法生成结果元素比以流的形式返回它们更容易时。
我们可以看到它现在具有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);
Run Code Online (Sandbox Code Playgroud)
它在int值溢出时停止,如上所述,当我们使用不消耗所有值的终端操作时它不会短路,但是,这个循环产生然后被忽略的值可能仍然比其他方法更快。
另一个受此答案启发的示例,从根到最具体的类层次结构进行迭代:
Stream.of(LinkedHashMap.class).mapMulti(MapMultiExamples::hierarchy)
/* additional stream operations here */
.forEach(System.out::println);
}
Run Code Online (Sandbox Code Playgroud)
static void hierarchy(Class<?> cl, Consumer<? super Class<?>> co) {
if(cl != null) {
hierarchy(cl.getSuperclass(), co);
co.accept(cl);
}
}
Run Code Online (Sandbox Code Playgroud)
与旧方法不同,它不需要额外的堆存储,并且可能运行得更快(假设合理的类深度不会使递归适得其反)。
同样的怪物像这样
Run Code Online (Sandbox Code Playgroud)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()
.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());
Run Code Online (Sandbox Code Playgroud)
与嵌套flatMap步骤相比,它失去了一些并行性机会,参考实现无论如何都没有利用这些机会。对于像上面这样的非短路操作,新方法可能会受益于减少的装箱和捕获 lambda 表达式的更少实例化。但是,当然,应该明智地使用它,而不是将每个构造都重写为命令式版本(在这么多人试图将每个命令式代码重写为函数式版本之后)……
| 归档时间: |
|
| 查看次数: |
1147 次 |
| 最近记录: |