何时以及如何执行一对 0..n 映射 Stream mapMulti over flatMap
When and how to perform one to 0..n mapping Stream mapMulti over flatMap
我一直在浏览新闻和最新 LTE Java 17 版本的源代码,我遇到了名为 mapMulti
的新 Stream 方法。早期访问 JavaDoc 说它类似于 flatMap
.
<R> Stream<R> mapMulti(BiConsumer<? super T,? super Consumer<R>> mapper)
- 如何使用这种方法进行一对0..n映射?
- 新方法如何工作以及它与
flatMap
有何不同。每个人什么时候更可取?
mapper
可以调用多少次?
Stream::mapMulti
是一种新方法class化为中间操作.
需要 BiConsumer<T, Consumer<R>> mapper
个即将处理的元素 Consumer
。后者使该方法乍一看很奇怪,因为它不同于我们习惯的其他中间方法,例如 map
、filter
或 peek
,其中 none 他们使用 *Consumer
.
的任何变体
API 本身在 lambda 表达式中提供的 Consumer
的目的是接受 any 数字元素,以便在后续管道。因此,将传播所有元素,无论有多少。
使用简单片段进行解释
一对多 (0..1) 映射(类似于filter
)
仅对少数选定项目使用 consumer.accept(R r)
可实现 filter-alike 管道。如果根据谓词 和 检查元素映射到不同的值,这可能会很有用,否则可以使用 filter
和 [=24= 的组合来完成] 反而。以下
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", "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)
any 次。假设我们要复制代表字符串长度的数字本身,即 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
与flatMap的比较
这个机制的真正想法是它可以被多次调用(包括零次)并且它对 SpinedBuffer
的使用在内部允许 push 元素到一个与 flatMap
不同,无需为每组输出元素创建一个新的单个扁平化 Stream 实例。 JavaDoc 声明 two use-cases 使用此方法优于 flatMap
:
- When replacing each stream element with a small (possibly zero) number of elements. Using this method avoids the overhead of creating a new Stream instance for every group of result elements, as required by flatMap.
- When it is easier to use an imperative approach for generating result elements than it is to return them in the form of a Stream.
Performance-wise,新方法 mapMulti
在这种情况下是赢家。查看此答案底部的基准。
Filter-map场景
单独使用此方法而不是 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, 2.0, 3.0, 4F, 5, 6L)
.filter(number -> number instanceof Integer)
.mapToInt(number -> (Integer) number)
.sum();
如上所示,它的变体如mapMultiToDouble
, mapMultiToInt
and mapMultiToLong
were introduced. This comes along the mapMulti
methods within the primitive Streams such as IntStream mapMulti(IntStream.IntMapMultiConsumer mapper)
。此外,还引入了三个新的功能接口。基本上,它们是 BiConsumer<T, Consumer<R>>
的原始变体,例如:
@FunctionalInterface
interface IntMapMultiConsumer {
void accept(int value, IntConsumer ic);
}
结合真实use-case场景
这种方法的真正强大之处在于它的使用灵活性和一次只创建一个 Stream,这是优于 flatMap
的主要优势。以下两个片段表示 Product
及其 List<Variation>
到 0..n
的平面映射,由 Offer
class 表示并基于特定条件(产品类别和变体可用性)。
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());
与使用 flatMap
、map
的 previous-versions 流方法组合的声明性方法相比,mapMulti
的使用更倾向于命令式和 filter
。从这个角度来说,就看use-case使用命令式的方式是否更容易了。递归是 JavaDoc.
中描述的一个很好的例子
基准
如约,我根据评论中收集的想法写了一堆micro-benchmarks。只要有相当多的代码要发布,我已经创建了一个包含实现细节的 GitHub repository,我将只分享结果。
Stream::flatMap(Function)
对比 Stream::mapMulti(BiConsumer)
Source
在这里我们可以看到巨大的差异,并证明新方法确实如描述的那样工作,并且它的使用避免了为每个处理过的元素创建一个新的 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)
对比 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)
与 Optional::stream()
对比 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
解决场景
When it is easier to use an imperative approach for generating result elements than it is to return them in the form of a Stream.
我们可以看到它现在具有 the yield statement C# 的有限变体。限制是我们总是需要来自流的初始输入,因为这是一个中间操作,此外,我们在一个函数评估中推送的元素没有 short-circuiting。
尽管如此,它还是带来了有趣的机会。
例如,实现以前需要的斐波那契数流 。
现在,我们可以使用类似的东西:
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
值溢出时停止,如前所述,它不会 short-circuit 当我们使用不消耗所有值的终端操作时,但是,此循环产生 then-ignored values 可能仍然比其他方法更快。
受 启发的另一个示例,用于从根到最具体的 class 层次结构迭代:
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);
}
}
与旧方法不同,它不需要额外的堆存储,并且可能 运行 更快(假设合理的 class 深度不会使递归适得其反)。
还有怪物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, j, k));
}
}
})
.collect(Collectors.toList());
与嵌套的 flatMap
步骤相比,它失去了一些并行机会,而参考实现并没有利用这些机会。对于像上面这样的非 short-circuiting 操作,新方法可能会受益于减少装箱和捕获 lambda 表达式的实例化。但是,当然,应该明智地使用它,而不是将每个构造都重写为命令式版本(在这么多人试图将每个命令式代码重写为函数式版本之后)……
我一直在浏览新闻和最新 LTE Java 17 版本的源代码,我遇到了名为 mapMulti
的新 Stream 方法。早期访问 JavaDoc 说它类似于 flatMap
.
<R> Stream<R> mapMulti(BiConsumer<? super T,? super Consumer<R>> mapper)
- 如何使用这种方法进行一对0..n映射?
- 新方法如何工作以及它与
flatMap
有何不同。每个人什么时候更可取? mapper
可以调用多少次?
Stream::mapMulti
是一种新方法class化为中间操作.
需要 BiConsumer<T, Consumer<R>> mapper
个即将处理的元素 Consumer
。后者使该方法乍一看很奇怪,因为它不同于我们习惯的其他中间方法,例如 map
、filter
或 peek
,其中 none 他们使用 *Consumer
.
API 本身在 lambda 表达式中提供的 Consumer
的目的是接受 any 数字元素,以便在后续管道。因此,将传播所有元素,无论有多少。
使用简单片段进行解释
一对多 (0..1) 映射(类似于
filter
)仅对少数选定项目使用
consumer.accept(R r)
可实现 filter-alike 管道。如果根据谓词 和 检查元素映射到不同的值,这可能会很有用,否则可以使用filter
和 [=24= 的组合来完成] 反而。以下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", "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)
any 次。假设我们要复制代表字符串长度的数字本身,即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
与flatMap的比较
这个机制的真正想法是它可以被多次调用(包括零次)并且它对 SpinedBuffer
的使用在内部允许 push 元素到一个与 flatMap
不同,无需为每组输出元素创建一个新的单个扁平化 Stream 实例。 JavaDoc 声明 two use-cases 使用此方法优于 flatMap
:
- When replacing each stream element with a small (possibly zero) number of elements. Using this method avoids the overhead of creating a new Stream instance for every group of result elements, as required by flatMap.
- When it is easier to use an imperative approach for generating result elements than it is to return them in the form of a Stream.
Performance-wise,新方法 mapMulti
在这种情况下是赢家。查看此答案底部的基准。
Filter-map场景
单独使用此方法而不是 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, 2.0, 3.0, 4F, 5, 6L)
.filter(number -> number instanceof Integer)
.mapToInt(number -> (Integer) number)
.sum();
如上所示,它的变体如mapMultiToDouble
, mapMultiToInt
and mapMultiToLong
were introduced. This comes along the mapMulti
methods within the primitive Streams such as IntStream mapMulti(IntStream.IntMapMultiConsumer mapper)
。此外,还引入了三个新的功能接口。基本上,它们是 BiConsumer<T, Consumer<R>>
的原始变体,例如:
@FunctionalInterface
interface IntMapMultiConsumer {
void accept(int value, IntConsumer ic);
}
结合真实use-case场景
这种方法的真正强大之处在于它的使用灵活性和一次只创建一个 Stream,这是优于 flatMap
的主要优势。以下两个片段表示 Product
及其 List<Variation>
到 0..n
的平面映射,由 Offer
class 表示并基于特定条件(产品类别和变体可用性)。
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());
与使用 flatMap
、map
的 previous-versions 流方法组合的声明性方法相比,mapMulti
的使用更倾向于命令式和 filter
。从这个角度来说,就看use-case使用命令式的方式是否更容易了。递归是 JavaDoc.
基准
如约,我根据评论中收集的想法写了一堆micro-benchmarks。只要有相当多的代码要发布,我已经创建了一个包含实现细节的 GitHub repository,我将只分享结果。
Stream::flatMap(Function)
对比 Stream::mapMulti(BiConsumer)
Source
在这里我们可以看到巨大的差异,并证明新方法确实如描述的那样工作,并且它的使用避免了为每个处理过的元素创建一个新的 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)
对比 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)
与 Optional::stream()
对比 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
解决场景
When it is easier to use an imperative approach for generating result elements than it is to return them in the form of a Stream.
我们可以看到它现在具有 the yield statement C# 的有限变体。限制是我们总是需要来自流的初始输入,因为这是一个中间操作,此外,我们在一个函数评估中推送的元素没有 short-circuiting。
尽管如此,它还是带来了有趣的机会。
例如,实现以前需要的斐波那契数流
现在,我们可以使用类似的东西:
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
值溢出时停止,如前所述,它不会 short-circuit 当我们使用不消耗所有值的终端操作时,但是,此循环产生 then-ignored values 可能仍然比其他方法更快。
受
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);
}
}
与旧方法不同,它不需要额外的堆存储,并且可能 运行 更快(假设合理的 class 深度不会使递归适得其反)。
还有怪物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, j, k));
}
}
})
.collect(Collectors.toList());
与嵌套的 flatMap
步骤相比,它失去了一些并行机会,而参考实现并没有利用这些机会。对于像上面这样的非 short-circuiting 操作,新方法可能会受益于减少装箱和捕获 lambda 表达式的实例化。但是,当然,应该明智地使用它,而不是将每个构造都重写为命令式版本(在这么多人试图将每个命令式代码重写为函数式版本之后)……