StreamAPIについて調べてみた reduce編 その1

【前提条件】

[環境]

【概要】

今回はreduceについて調べてみました。

reduceはStreamの要素を集計した結果に対して、
何かしらの処理行い一つの結果を得るメソッドです。

たとえば、List内の要素の合計を求めるなどを行う場合に使います。

【reduceメソッド】

Stream#reduceメソッドには3種類用意されています。

	Optional<T> reduce(BinaryOperator<T> accumulator);
	T reduce(T identity, BinaryOperator<T> accumulator);
	<U> U reduce(U identity,
                 BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);

パラメータがそれぞれ1個〜3個のメソッドが用意されています。
一つずつ見ていきます。

今回のサンプルは文字列を結合するものを作成します。

【パラメータが1つのメソッド】

[サンプルソース]
    public static void main(final String[] args) {

        final List<String> list = new ArrayList<>();

        list.add("one");
        list.add("two");
        list.add("three");

        final Optional<String> result = list.stream().reduce(
            (accum, value) -> {
                return accum + "-"  + value;
            });

        System.out.println(result.orElse(""));
    }

BinaryOperatorのオブジェクトがパラメータなります。
内部では途中結果を保持するためのアキュムレータと
要素の値がパラメータとして受け取る、BinaryOperatorのオブジェクトが生成されます。

最初にreduceが実行される段階ではaccumが"one"、valueが"two"で実行されます。
2回目はaccumが"one-two"、valueが"three"で実行されます。

戻り値はOptionalでTはStream内オブジェクトと同じ型になります。

[実行結果]
one-two-three

【パラメータが2つのメソッド】

[サンプルソース]
    public static void main(final String[] args) {

        final List<String> list = new ArrayList<>();

        list.add("one");
        list.add("two");
        list.add("three");

        final BinaryOperator<String> operation = 
            (accum, value) -> {
                return accum + "-"  + value;
            };

        final String result1 = list.stream().reduce("value", operation);
        final String result2 = new ArrayList<String>().stream().reduce("value", operation);

        System.out.println(result1);
        System.out.println(result2);
    }

パラメータが2つの場合はStream内要素と同じ型のオブジェクトと
BinaryOperatorのオブジェクトがパラメータなります。

1つめパラメータはアキュムレータの初期値になります。
パラメータが2つの場合は1つめのパラメータに渡した値が
アキュムレータの初期値になります。
パラメータが1つの場合は自動的に最初の要素が
アキュムレータの初期値となっていました。

2つめのパラメータは実際に処理を行うBinaryOperatorです。
これはパラメータが1つの時と変わりません。

戻り値の方はStream内のオブジェクトと同じ型が帰ってきます。

Optionalになっていないのは
おそらく最初に渡したパラメータの値は設定されるので、
Nullの可能性がないためだと思います。
(明示的にNullを渡した場合はNullPointerExceptionが発生しましが・・・)

[実行結果]
value-one-two-three
value

【パラメータが3つのメソッド】

[サンプルソース]
    public static void main(final String[] args) {        
        
        final List<String> list = new ArrayList<>();

        list.add("one");
        list.add("two");
        list.add("three");
        

        final BiFunction<String, String, String> operation = 
            (accum, value) -> {
                return accum + "-"  + value;
            };
        final BinaryOperator<String> combiner =
            (value1, value2) -> {
                System.out.println("combine!");
                return value1 + "+" + value2;
            };
        
        final String result1 = list.stream().reduce("value", operation, combiner);
        System.out.println(result1);

        System.out.println("=====================================================");

        final String reuslt2 = list.stream().parallel().reduce("value", operation, combiner);
        System.out.println(reuslt2);
    }

パラメータが3つの場合は↓の値を渡します。
 1:Stream内要素と同じ型のオブジェクト
 2:パラメータ、戻り値が全て1の型と同じBiFunctionオブジェクト
 3:型パラメータが1の型と同じBinaryOperatorオブジェクト

1はアキュムレータの初期値です。
パラメータが2つの時と同様です。

2は単一の結果を得るための処理を行うFiFunctionオブジェクトです。
パラメータが2つだった場合の、BinaryOperatorオブジェクトとほぼ同じです。
Stream#paralleメソッドで実行した際に動きがわかるので要注意です。

3はStream#paralleメソッドで並行処理が行われる場合にのみ実行されます。
BinaryOperatorのパラメータには2の実行結果の値が2つ渡されます。

[実行結果]
value-one-two-three
=====================================================
combine!
combine!
value-one+value-two+value-three

シリアルに処理している場合はoperation内部の処理が実行されて
各値が"-"で結合されているのがわかります。

並行に処理している場合、2回ほど"combine!"が出力されているので、
combiner内の処理が行われているのがわかります。
処理結果がシリアルに処理している場合と異なります。

並行処理の場合は2のBiFunctionオブジェクトには
1の値とStream内のオブジェクトがパラメータとして渡されるようです。
サンプルでは"value"とStream内のオブジェクトが渡されます。

2の実行した結果を3のBinaryOperatorのパラメータとして
受け取り実行されます。

【まとめ】

reduceメソッドはStream内のオブジェクトに対して
何かしらの加工・計算を行い、集計した結果が帰ってくることがわかりました。

一つ問題なのはパラメータが3つの場合で
並行処理を行う場合、パラメータの役割が変わってしまうので
注意が必要になることでしょうか。

今回のサンプルが悪かったかなと思う部分もありますが・・・

数値計算であれば、アキュムレータに0を渡せば
影響はなく処理を行えると思うので、
アキュムレータに何を渡すかは慎重に考える必要がありそうですね。