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を使って計算する
Stream
とCollector
を組み合わせて使うと『並列処理による効率化』が簡単に実行できる。
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
で与えて、演算処理をStream
のcollect
メソッドで実行する。
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);
}
}