LoginSignup
1
2

StreamのmapMultiはどういう時に使うのか

Posted at

Java16で追加されたStream.mapMultiメソッドの説明が、最初はよくわからなかったので、理解したことを少し具体例を挙げてまとめてみます。

前置き

実は、最近までJava9より後のJavaの動向を追ってなかったので、Java21までの変更をさらっと読んできました。

参考資料

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ドキュメントに載っているので省きます。

filtermapをまとめられるのが地味に嬉しいかも。反面、複数行になってしまいがちですね。

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個
1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2