Stream API中的mapMulti指南

1. 概述

在本教程中,我們將回顧Stream::mapMulti方法。我們將編寫簡單的示例來說明如何使用它。特別是,我們會看到這個方法類似於Stream:: flatMap我們將介紹在什麼情況下我們更喜歡使用mapMulti不是flatMap

請務必查看我們關於Java Streams的文章,以更深入地了解 Stream API。

2. 方法簽名

省略通配符, mapMulti方法可以寫得更簡潔:

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

這是一個Stream中間操作。 BiConsumer功能接口的實現作為參數。 BiConsumer的實現採用Stream元素T ,如有必要,將其轉換為type R ,並調用mapper'Consumer::accept

在 Java 的mapMulti方法實現中, mapper是一個緩衝區,它實現了Consumer功能接口。

每次我們調用Consumer::accept,它都會累積緩衝區中的元素並將它們傳遞給流管道。

3. 簡單實現示例

讓我們考慮一個整數列表來執行以下操作:

List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
 double percentage = .01;
 List<Double> evenDoubles = integers.stream()
 .<Double>mapMulti((integer, consumer) -> {
 if (integer % 2 == 0) {
 consumer.accept((double) integer * ( 1 + percentage));
 }
 })
 .collect(toList());

BiConsumer<T, Consumer<R>> mapper的 lambda 實現中,我們首先只選擇偶數整數,然後我們將指定的數量添加到percentage ,將結果轉換為double,精度數,並完成調用consumer.accept

正如我們之前看到的, consumer只是一個緩衝區,它將返回元素傳遞給流管道。 (作為旁注,請注意我們必須使用類型見證<Double>mapMulti作為返回值,否則編譯器無法在方法簽名中R

根據元素是奇數還是偶數,這是一對零或一對一轉換。

請注意,前面代碼示例中if statement Stream::filter的角色,並將整數轉換為 double,扮演Stream::map的角色。因此,我們可以使用Stream's filtermap來實現相同的結果:

List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
 double percentage = .01;
 List<Double> evenDoubles = integers.stream()
 .filter(integer -> integer % 2 == 0)
 .<Double>map(integer -> ((double) integer * ( 1 + percentage)))
 .collect(toList());

然而, mapMulti實現更直接,因為我們不需要調用這麼多的流中間操作

另一個優點是**mapMulti實現是必要的,讓我們可以更自由地進行元素轉換**。

為了支持intlongdouble原始類型,我們有mapMultiToDoublemapMultiToInt,mapMultiToLong的變化mapMulti

例如,我們可以使用mapMultiToDouble來求上一個雙精度List

List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
 double percentage = .01;
 double sum = integers.stream()
 .mapMultiToDouble((integer, consumer) -> {
 if (integer % 2 == 0) {
 consumer.accept(integer * (1 + percentage));
 }
 })
 .sum();

4. 更現實的例子

讓我們考慮一個Album的集合:

public class Album {

 private String albumName;
 private int albumCost;
 private List<Artist> artists;

 Album(String albumName, int albumCost, List<Artist> artists) {
 this.albumName = albumName;
 this.albumCost = albumCost;
 this.artists = artists;
 }
 // ...
 }

每個Album都有一個Artist列表:

public class Artist {

 private final String name;
 private boolean associatedMajorLabels;
 private List<String> majorLabels;

 Artist(String name, boolean associatedMajorLabels, List<String> majorLabels) {
 this.name = name;
 this.associatedMajorLabels = associatedMajorLabels;
 this.majorLabels = majorLabels;
 }
 // ...
 }

如果我們想收集藝術家-專輯名稱對的列表,我們可以使用mapMulti來實現它:

List<Pair<String, String>> artistAlbum = albums.stream()
 .<Pair<String, String>> mapMulti((album, consumer) -> {
 for (Artist artist : album.getArtists()) {
 consumer.accept(new ImmutablePair<String, String>(artist.getName(), album.getAlbumName()));
 }
 })

對於流中的每個專輯,我們遍歷藝術家,創建ImmutablePair ,並調用 C onsumer::acceptmapMulti的實現累積消費者接受的元素並將它們傳遞給流管道。

這具有一對多轉換的效果,其中結果在消費者中累積,但最終被扁平化為新的流。這本質上是Stream::flatMap所做的,以便我們可以通過以下實現獲得相同的結果:

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

我們看到這兩種方法給出了相同的結果。接下來我們將介紹在哪些情況下使用mapMulti更有利。

5. 何時使用mapMulti而不是flatMap

5.1.用少量元素替換流元素

正如 Java 文檔中所述:“當用少量(可能為零)元素替換每個流元素時。使用這種方法可以避免為每組結果元素Stream flatMap”.

讓我們寫一個簡單的例子來說明這個場景:

int upperCost = 9;
 List<Pair<String, String>> artistAlbum = albums.stream()
 .<Pair<String, String>> mapMulti((album, consumer) -> {
 if (album.getAlbumCost() < upperCost) {
 for (Artist artist : album.getArtists()) {
 consumer.accept(new ImmutablePair<String, String>(artist.getName(), album.getAlbumName()));
 }
 }
 })

對於每張專輯,我們迭代藝術家並積累零個或幾個藝術家-專輯對,這取決於專輯的價格與變量upperCost相比。

flatMap完成相同的結果:

int upperCost = 9;
 List<Pair<String, String>> artistAlbum = albums.stream()
 .flatMap(album -> album.getArtists()
 .stream()
 .filter(artist -> upperCost > album.getAlbumCost())
 .map(artist -> new ImmutablePair<String, String>(artist.getName(), album.getAlbumName())))
 .collect(toList());

我們看到**mapMulti的命令式實現的flatMap的聲明式方法那樣為每個處理過的元素創建中間流**。

5.2.何時更容易生成結果元素

讓我們在Album類中編寫一個方法,將所有藝術家-專輯對及其相關的主要標籤傳遞給消費者:

public class Album {

 //...
 public void artistAlbumPairsToMajorLabels(Consumer<Pair<String, String>> consumer) {

 for (Artist artist : artists) {
 if (artist.isAssociatedMajorLabels()) {
 String concatLabels = artist.getMajorLabels().stream().collect(Collectors.joining(","));
 consumer.accept(new ImmutablePair<>(artist.getName()+ ":" + albumName, concatLabels));
 }
 }
 }
 // ...
 }

如果藝術家與主要標籤有關聯,則實現會將標籤連接到逗號分隔的字符串中。然後它創建一對帶有標籤的藝術家專輯名稱並調用 C onsumer::accept

如果我們想獲得所有對的列表,就像使用mapMulti和方法引用Album::artistAlbumPairsToMajorLabels一樣簡單:

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

我們看到,在更複雜的情況下,我們可以有非常複雜的方法引用實現。例如, Java 文檔給出了一個使用遞歸的示例。

latMap複製相同的結果會非常困難。 **flatMap .**要求以Stream的形式返回它們更容易的情況下,我們應該使用mapMulti **flatMap** .