2
0

【Java Gold🥇】Collectorのつかいかた(Java SE 11 -)

Last updated at Posted at 2023-07-23

java.util.stream.Collectorを使うと、配列のすべての要素どうしを計算する処理(数列の総和計算など)を効率化できるようだ。

総和計算を効率化したい

Q. 数が$+$で連結されている式$1+2+ \cdots +10$を効率よく(計算ステップ数が少なくなるように)3人で分担して計算するにはどのようにすればいいですか?
A. 以下の表のように『並列処理による効率化』をして計算すると4ステップで計算できる。

計算者\ステップ数 1 2 3 4
A $a_1=1+2$ $a_2=a_1+3$ $a_3=a_2+b_2$ $\sigma=a_3+c_3$
B $b_1=4+5$ $b_2=b_1+6$ - -
C $c_1=7+8$ $c_2=c_1+9$ $c_3=c_2+10$ -

この効率化は、与えられた式
$\sigma = 1+2+3+4+5+6+7+8+9+10$

$\sigma = (((1+2)+3)+((4+5)+6))+(((7+8)+9)+10)$
のように、式をいくつかのまとまりに分けて計算したことを意味する。

一般的な処理に応用する

$+$以外の演算子で連結されている式に対しても同じ『並列処理による効率化』が使えるだろうか?
ここで一般化した演算$\circ$に対して考えてみよう。

演算$\circ$における、総和計算に代わる計算(処理)は
$X = a_1 \circ a_2 \circ \cdots \circ a_n$
といった表現になる。

先ほどの効率化方法を行うためには演算をどのようなまとまりに分割しても全体の結果は変わってはいけない。 したがって演算$\circ$が『並列処理による効率化』可能である条件は結合法則 $(a \circ b) \circ c = a \circ (b \circ c)$ を満たすことといえる。

結合法則を満たしさえすればどんな演算でも演算子で連結された式の計算は『並列処理による効率化』ができる。

総和の式をCollectorを使って計算する

StreamCollectorを組み合わせて使うと『並列処理による効率化』が簡単に実行できる。
Collectorインスタンスを作成するには、以下の表にある3つの型を決め、それに応じた4つの処理を定義する必要がある。

使用する3つの型

ジェネリクス 内容 総和計算でいうと…
T 処理の入力要素の型 $1,2,\cdots$といった数値
A 処理の中間状態を保持するオブジェクトの型 $(1+2)$といった部分的演算結果
R 処理結果の型 総和計算の結果の数値

実装する4つの処理

処理名 入力 出力 処理内容 総和計算でいうと…
supplier Supplier<A> - A 新しい結果コンテナの作成 $()$の枠を作る
accumulator BiConsumer<A,T> A, T - 結果コンテナAへの新しいデータ要素Tの組み込み $(1+2)+3$
combiner BinaryOperator<A> A,A A 2つの結果コンテナAを1つに結合する $(1+2)+(3+4)$
finisher Function<A,R> A R 結果コンテナAを最終的な出力に変換する 計算結果を出力する

4つの処理を実装したらCollectorインスタンスを生成しよう。
Collectorファクトリメソッドであるofメソッドで生成できる。

Collector<T, A, R> collector = Collector.of(supplier, accumulator, combiner, finisher,	characteristics);

ofメソッドの最後の引数は並行処理に対する設定らしい。
Characteristics.CONCURRENT

$a_1, a_2, \cdots , a_n$の列をStreamで与えて、演算処理をStreamcollectメソッドで実行する。

Stream<T> stream;
R result = stream.collect(collector);

Integerの総和計算の実装

class Test {
	Integer calcSum(List<Integer> list) {
        // 結果コンテナ
		class IntContainer {
			public int value;

			IntContainer() {
				this(0);
			}

			IntContainer(int value) {
				this.value = value;
			}
		}

        // 新しい結果コンテナの作成
		Supplier<IntContainer> supplier = IntContainer::new;

        // 結果コンテナ`IntContainer`への新しいデータ要素`Integer`の組み込み
		BiConsumer<IntContainer, Integer> accumulator = (container, i) -> container.value += i;

        // 2つの結果コンテナ`IntContainer`を1つに結合する
		BinaryOperator<IntContainer> combiner = (a, b) -> new IntContainer(a.value + b.value);

        // 結果コンテナ`IntContainer`を最終的な出力に変換する
		Function<IntContainer, Integer> finisher = container -> container.value;

		// 並行処理を前提とする
		Characteristics characteristics = Characteristics.CONCURRENT;

        // Collectorを生成
		Collector<Integer, IntContainer, Integer> collector = Collector.of(supplier, accumulator, combiner, finisher,	characteristics);

        // Collectorによる処理を実行する
		Stream<Integer> stream = list.stream().parallel();
		return stream.collect(collector);
	}
}

実装例:複数の文字列をカンマで連結する

複数の文字列をカンマ区切りで連結してひとつの文字列で出力する処理を考える。
文字の連結も結合法則を満たす処理なので、Collectorで処理可能である。

文字の連結操作+で連結するよりStringBuilderを使ったほうが処理速度が向上する。
したがって、この処理は『処理の中間状態を保持するオブジェクトの型A』にStringBuilderを使う。

class Test {
	String test() {
		// 処理の初めに行う初期化:StringBuilderのインスタンスを生成する
		Supplier<StringBuilder> supplier = () -> new StringBuilder();

		// 次の要素を取り込み処理する:StringをStringBuilderに追加する
		BiConsumer<StringBuilder, String> accumulator = (sb, str) -> {
			if (sb.length() > 0)
				sb.append(", ");
			sb.append(str);
		};

		// まとまりの処理を結合する処理:StringBuilderどうしを連結する
		BinaryOperator<StringBuilder> combiner = (a, b) -> {
			if (a.length() > 0)
				a.append(", ");
			return a.append(b);
		};

		// 中間生成物を結果に変換する処理:StringBuilderで文字列を生成する
		Function<StringBuilder, String> finisher = sb -> {
			return sb.toString();
		};

		// 並行処理を前提とする
		Characteristics characteristics = Characteristics.CONCURRENT;

        // Collectorを生成
		Collector<String, StringBuilder, String> collector = Collector.of(supplier, accumulator, combiner, finisher, characteristics);

        // Collectorによる処理を実行する
    	Stream<String> stream = list.stream().parallel();
		return stream.collect(collector);
	}
}
2
0
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
2
0