问题描述
我一直在浏览Java 16的新闻和源代码,并且遇到了名为mapMulti
的新Stream方法。抢先体验版JavaDoc说它与flatMap
类似,并且已经被批准使用相同的Java版本。
<R> Stream<R> mapMulti(BiConsumer<? super T,? super Consumer<R>> mapper)
解决方法
Stream::mapMulti
是一种新方法,被归类为中间操作。
它需要BiConsumer<T,Consumer<R>> mapper
的元素Consumer
即将被处理。乍一看,后者使该方法看起来很奇怪,因为它与我们在其他map
,filter
或peek
等其他中间方法中所用的方法不同使用*Consumer
的任何变体形式。
API本身在lambda表达式中提供的Consumer
的目的是接受任何个数字元素,以便在后续管道中使用。因此,所有元素,无论有多少,都将被传播。
使用简单摘要进行解释
-
一对一(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
-
一对一映射(类似于
map
)使用上一个示例,当省略条件并将每个元素映射到一个新元素并使用
consumer
接受时,该方法的行为类似于map
:Stream.of("Java",consumer) -> consumer.accept(str.length())) .forEach(i -> System.out.print(i + " ")); // 4 6 10 2 4
-
一对多映射(类似于
flatMap
)这里的事情变得很有趣,因为一个人可以多次拨打
consumer.accept(R r)
任意。假设我们要复制代表字符串长度的数字,即2
变成2
,2
。4
变为4
,4
,4
,4
。0
一无所有。Stream.of("Java","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
与flatMap的比较
该机制的基本思想是可以多次调用(包括零次),并且内部使用SpinedBuffer
可以将元素推到单个扁平化的Stream实例中,而无需与flatMap
不同,为每组输出元素创建一个新的。在使用此方法时,JavaDoc指出了两个用例,胜过flatMap
:
- 用少量(可能为零)元素替换每个流元素时。使用此方法避免了FlatMap要求为每组结果元素创建新的Stream实例的开销。
- 使用命令式方法生成结果元素比以Stream的形式返回结果元素更容易。
在性能方面,在这种情况下,新方法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
int sum = Stream.of(1,6L)
.filter(number -> number instanceof Integer)
.mapToInt(number -> (Integer) number)
.sum();
如上所述,引入了mapMultiToDouble
,mapMultiToInt
和mapMultiToLong
之类的变体。这与原始流(例如IntStream mapMulti(IntStream.IntMapMultiConsumer mapper)
)中的mapMulti
方法一起出现。此外,还引入了三个新的功能接口。基本上,它们是BiConsumer<T,Consumer<R>>
的原始变体,例如:
@FunctionalInterface
interface IntMapMultiConsumer {
void accept(int value,IntConsumer ic);
}
组合的实际用例场景
此方法的真正优势在于其使用的灵活性,并且一次只能创建一个Stream,这是优于flatMap
的主要优势。下面的两个摘录表示Product
及其List<Variation>
到0..n
类所表示的Offer
报价的统一映射,并基于某些条件(产品类别和变体可用性)。
-
Product
和String name
,int basePrice
,String category
和List<Variation> variations
。 -
Variation
和String name
,int price
和boolean 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方法组合的声明方法相比,mapMulti
的使用势在必行,后者在使用flatMap
,map
和{{1 }}。从这个角度看,是否更容易使用命令式方法取决于用例。递归是JavaDoc中描述的一个很好的例子。
基准
按照承诺,我已经从评论收集的想法中写下了一堆微基准。只要要发布的代码很多,我就创建了一个带有实现细节的GitHub repository,我将只分享结果。
filter
与Stream::flatMap(Function)
Source
在这里,我们可以看到巨大的区别,并证明了这种新方法实际上可以按所述方法工作,并且它的用法避免了为每个处理过的元素创建新Stream实例的开销。
Stream::mapMulti(BiConsumer)
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)
Source
使用链式管道(虽然不是嵌套的)是可以的。
Stream::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
和Stream::flatMap(Function)
与Optional::stream()
Source
这是一个非常有趣的过程,尤其是在用法方面(请参见源代码):现在我们可以使用Stream::mapMulti(BiConsumer)
进行展平,并且在这种情况下,新方法要快一些。
mapMulti(Optional::ifPresent)
,
解决方案
使用命令式方法生成结果元素要比以Stream形式返回结果元素容易得多。
我们可以看到它现在具有the yield statement C#的有限变体。局限性在于,我们总是需要来自流的初始输入,因为这是一个中间操作,而且,在一个函数求值中,我们要推送的元素不会发生短路。
还是,这会带来有趣的机会。
例如,实现以前需要a solution using temporary objects capable of holding two values的斐波那契数字流。
现在,我们可以使用类似的东西:
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
值溢出时停止,如前所述,当我们使用不消耗所有值的终端操作时,它不会短路,但是,此循环产生然后被忽略的值可能仍然是比其他方法更快。
另一个受this answer启发的示例,它从根到最具体的遍历一个类层次结构:
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);
}
}
与旧方法不同,它不需要额外的堆存储,并且运行速度可能更快(假设合理的类深度不会造成递归回火)。
还有怪物like this
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,k));
}
}
})
.collect(Collectors.toList());
与嵌套的flatMap
步骤相比,它失去了一些并行的机会,而参考实现无论如何都没有利用它。对于如上所述的非短路操作,新方法可能会受益于减少装箱和捕获lambda表达式的实例化较少的情况。但是,当然,应该明智地使用它,而不是将每个构造都重写为命令性版本(在许多人试图将每个命令性代码重写为功能版本之后)……