1. 概要

このチュートリアルでは、Java16で導入されたメソッドStream ::mapMultiを確認します。 使い方を説明する簡単な例を書きます。 特に、このメソッドはStream ::flatMapに似ていることがわかります。 flatMapよりもmapMultiを使用することを好む状況について説明します。

Stream APIの詳細については、 JavaStreamsに関する記事を確認してください。

2. メソッドシグネチャ

ワイルドカードを省略すると、mapMultiメソッドをより簡潔に記述できます。

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

これはStream中間操作です。 パラメータとして、BiConsumer機能インターフェイスの実装が必要です。 BiConsumerの実装は、必要に応じてStream要素Tを受け取り、それをタイプRに変換し、マッパーの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 >マッパー 、最初に偶数の整数のみを選択し、次にで指定された量をそれらに追加しますパーセンテージ 、結果をにキャストしますダブル、 呼び出しを終了します Consumer.accept

前に見たように、 Consumer は、戻り要素をストリームパイプラインに渡す単なるバッファーです。 (補足として、タイプ証人を使用する必要があることに注意してください mapMulti そうしないと、コンパイラは正しいタイプを推測できないため、戻り値として R メソッドのシグネチャで。)

これは、要素が奇数か偶数かに応じて、1対0または1対1の変換になります。

前のコードサンプルのifステートメントStream:: filter の役割を果たし、整数をdoubleにキャストすることに注意してください。Stream::の役割です。マップ。 したがって、Streamの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の実装は、それほど多くのストリーム中間操作を呼び出す必要がないため、より直接的です。

もう1つの利点は、 mapMultiの実装が不可欠であり、要素変換をより自由に実行できることです。

int long 、および double プリミティブ型をサポートするために、 mapMultiToDouble mapMultiToInt、、およびがあります。 ] mapMultiToLong mapMultiのバリエーション。

たとえば、 mapMultiToDouble を使用して、以前のListのdoubleの合計を見つけることができます。

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. より現実的な例

アルバムのコレクションを考えてみましょう。

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;
    }
    // ...
}

アルバムには、アーティストのリストがあります。

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()));
      }
  })

ストリーム内のアルバムごとに、アーティストを反復処理し、アーティストアルバム名のApache Commons ImmutablePair を作成し、C onsumer ::acceptを呼び出します。 mapMulti の実装は、コンシューマーによって受け入れられた要素を蓄積し、それらをストリームパイプラインに渡します。

これには、結果がコンシューマーに蓄積されるが、最終的には新しいストリームにフラット化される1対多の変換の効果があります。 これは基本的に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. flatMapの代わりにmapMultiを使用する場合

5.1. ストリーム要素を少数の要素に置き換える

Javaのドキュメントに記載されているように、「各ストリーム要素を少数の(場合によってはゼロの)要素に置き換える場合。 このメソッドを使用すると、 flatMap “で必要とされる、結果要素のグループごとに新しいStreamインスタンスを作成するオーバーヘッドを回避できます。

このシナリオを説明する簡単な例を書いてみましょう。

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ドキュメントは、再帰を使用した例を示しています。

一般に、f latMapを使用して同じ結果を複製することは非常に困難です。 したがって、結果要素の生成がflatMapで必要なストリームの形式で返すよりもはるかに簡単な場合はmapMultiを使用する必要があります。

6. 結論

このチュートリアルでは、さまざまな例を使用してmapMultiを実装する方法について説明しました。 flatMap と比較して、使用する方が有利な場合を確認しました。

特に、いくつかのストリーム要素を置き換える必要がある場合、または命令型アプローチを使用してストリームパイプラインの要素を生成する方が簡単な場合は、mapMultiを使用することをお勧めします。

ソースコードはGitHubにあります。