Java16で追加されたStream.mapMulti
メソッドの説明が、最初はよくわからなかったので、理解したことを少し具体例を挙げてまとめてみます。
前置き
実は、最近までJava9より後のJavaの動向を追ってなかったので、Java21までの変更をさらっと読んできました。
参考資料
-
Java 16新機能まとめ
https://qiita.com/nowokay/items/215769cdcb14d6c5412f -
java.util.stream.Stream - Java17 API ドキュメント
https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/util/stream/Stream.html
APIの説明にある「flatMap
」との関係とは
APIドキュメントにはこのように書いてあります。
APIのノート:
このメソッドは、ストリームの要素に一対多変換を適用し、結果要素を新しいストリームにフラット化するという点で、flatMapに似ています。 このメソッドは、次の場合にflatMapよりも推奨されます:
各ストリーム要素を少数の(0(ゼロ))要素で置換する場合。 このメソッドを使用すると、flatMapで必要な結果要素のグループごとに新しいStreamインスタンスを作成するオーバーヘッドが回避されます。
結果要素をStreamの形式で返すよりも、結果要素の生成に必須のアプローチを使用する方が簡単な場合。
これを読んだだけで理解できる方は、これ以降は読む必要はないと思います。
最初、新しいStreamインスタンスというのが何のことなのか分かりませんでした。
flatMap
メソッドを使ったことがあれば気づけそうですが、私は使う頻度はあまり高くないので、コード書いてみたらピンときました。
それぞれの要素にダッシュ要素を追加したリストを作るコードを書いてみます。(ダッシュだと見づらいのでスラッシュで代用)
jshell> var data = IntStream.rangeClosed(1, 3).mapToObj(String::valueOf).toList()
data ==> [1, 2, 3]
jshell> data.stream().flatMap(x -> Stream.of(x, x + "/")).toList()
$1 ==> [1, 1/, 2, 2/, 3, 3/]
Javaの場合、フラット化するには一対多変換にStreamしか使えないので、その時に生成するStreamインスタンスのことを言っていたんですね。
なるほど。
そして、mapMulti
メソッドを使うと、新しいStreamインスタンスを作らずに処理できるようになります。
型推論してくれないのがちょっと面倒ですね。
jshell> data.stream().<String>mapMulti((x, c) -> {
...> c.accept(x);
...> c.accept(x + "/");
...> }).toList()
$2 ==> [1, 1/, 2, 2/, 3, 3/]
ほかの例
再帰的に増幅するようなケースはflatMap
では難しそうですので、mapMulti
を使うとよさそうです。再帰の例はAPIドキュメントに載っているので省きます。
filter
とmap
をまとめられるのが地味に嬉しいかも。反面、複数行になってしまいがちですね。
jshell> Stream.of("AAA", "BB", "CCCCC").mapMulti((x, c) -> {
...> if (x.length() >= 3) c.accept("%s=>%d".formatted(x, x.length()));
...> }).toList()
$3 ==> [AAA=>3, CCCCC=>5]
新しいStreamインスタンスを作らないとパフォーマンスはどうなる?
簡易ベンチマークで試してみました。
少なくとも、新しいStreamインスタンスを作らないことで速くなっているみたいです。
List<String> data = IntStream.rangeClosed(1, 1_000_000).mapToObj(String::valueOf).toList();
for (int j = 1; j <= 3; j++) {
long t;
List<String> amplified;
t = System.currentTimeMillis();
System.out.print("flatMap ");
amplified = data.stream().flatMap(x -> Stream.of(x, x + "/")).toList();
t = System.currentTimeMillis() - t;
System.out.printf(" %d回目 %4dミリ秒 %d個%n", j, t, amplified.size());
t = System.currentTimeMillis();
System.out.print("mapMulti");
amplified = data.stream().<String>mapMulti((x, c) -> {
c.accept(x);
c.accept(x + "/");
}).toList();
t = System.currentTimeMillis() - t;
System.out.printf(" %d回目 %4dミリ秒 %d個%n", j, t, amplified.size());
}
flatMap 1回目 187ミリ秒 2000000個
mapMulti 1回目 68ミリ秒 2000000個
flatMap 2回目 152ミリ秒 2000000個
mapMulti 2回目 33ミリ秒 2000000個
flatMap 3回目 229ミリ秒 2000000個
mapMulti 3回目 61ミリ秒 2000000個