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

【前提条件】

[環境]

【概要】

前回はStreamAPIの使い方について調べました。

今回はStreamAPIのメソッドのうち、
collectを調べてみました。

collectはStreamAPIで実行した結果を
集計する機能です。

【Collectors】

Collectorsクラスはよく使用する機能がまとめられたクラスです。

toListメソッド、toSetメソッドなど結果をコレクションに変換する機能。
joiningメソッド、maxByなど集計した結果を加工する機能。

よく使われるであろう機能はまとめられています。
collecを使う場合には基本的にCollectorsを使うことになると思います。

前回のサンプルとほぼ同じですが、
streamを作ってcollectするソースコードを見てみます。

    public static void main(final String[] args) {

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

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

        final List<String> convertList = list.stream().collect(Collectors.toList());
        convertList.forEach(System.out::println);
    }

Collectors#toListで結果をListに変換します。
サンプルでは何もせずListにしているので無意味なソースですが。

【独自のCollectorクラス】

[Stream#collectメソッド]

collecctメソッドの定義を見てみましょう。

public interface Stream<T> extends BaseStream<T, Stream<T>> {

    <R, A> R collect(Collector<? super T, A, R> collector);
}

TはStreamを作成したコレクションの型です。
サンプルではStringです。

Aはアキュムレータの型を指定します。
アキュムレータは大雑把に説明すると途中結果を保持するためものです。

Rは戻り値の方を指定します。

パラメータにはCollectorインターフェイスのオブジェクトを受け取ります。

[Collector]

Collectorの動きを理解するために
文字列を数値に変換してSetで返すCollectorを作成します。

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;

public class SampleCollector implements Collector<String, List<Integer>, Set<Integer>> {

    @Override
    public Supplier<List<Integer>> supplier() {
        return () -> new ArrayList<>();
    }

    @Override
    public BiConsumer<List<Integer>, String> accumulator() {
        return (acumList, value) -> {

            switch(value) {
                case "one" : acumList.add(1);
                             break;
                case "two" : acumList.add(2);
                             break;
                case "three" : acumList.add(3);
                             break;
                default:  acumList.add(0);
            }
        };
    }

    @Override
    public BinaryOperator<List<Integer>> combiner() {
        return (returnList, colletedList) -> {
            System.out.println("combiner!");
            returnList.addAll(colletedList);
            return returnList;
        };
    }

    @Override
    public Function<List<Integer>, Set<Integer>> finisher() {
        return paramList -> {

            final Set<Integer> returnSet = new LinkedHashSet<>();

            returnSet.addAll(paramList);
            returnSet.add(10);

            return returnSet;
        };
    }

    @Override
    public Set<Collector.Characteristics> characteristics() {

        final Set<Collector.Characteristics> returnSet = new HashSet<>();

//      3つの指定ができますが、サンプルではOFFです。
//      returnSet.add(Characteristics.IDENTITY_FINISH);
//      returnSet.add(Characteristics.CONCURRENT);
//      returnSet.add(Characteristics.UNORDERED);

        return returnSet;
    }
}

supplierメソッドではSupplierインターフェイスのオブジェクトを返します。
supplierではアキュムレータの初期状態を作ります。
Supplierインターフェイスはパラメータなし、戻り値ありの関数型インターフェイスです。

accumulatorメソッドではコレクション内の各要素に対する処理を実装します。
パラメータはアキュムレータと各要素の値です。
サンプルでは文字列を数値に変換してアキュムレータのListにつめています。

combinerメソッドでは並列に処理を行ったときに実行される処理です。

finisherメソッドは最終的に実行される処理です。
パラメータとしてアキュムレータが渡されます。
サンプルではアキュムレータで渡されたListをSetに変換して、最後に10をつめています。

characteristicsメソッドはCollectorオブジェクトの特性を指定します。

[characteristics]

characteristicsメソッドで指定した値で↓のように動きが変わります。

IDENTITY_FINISHを設定するとfinisherメソッドが実行されなくなります。
finisherメソッドが実行されない代わりに、
最終的なアキュムレータが返却されるようになります。

今回のサンプルでこのオプションを指定すると戻り値はListになってしまいます。
アキュムレータと戻り値の型が異なる場合は指定してはだめなようです。

CONCURRENTとUNORDEREDを指定した場合、
並行に処理した時に処理順序が保障されなくなります。

↑のオプションを指定すると実行するたびに
結果も変わっているっぽいのはサンプルが悪いのかもしれません・・・

【呼び出し元】

では、作成したSampleCollectorクラスを使ってみます。

[ソース]
    public static void main(final String[] args) {

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

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

        SampleCollector sampleCollector = new SampleCollector();
        final Set<Integer> collectorList = list.stream().collect(sampleCollector);
        collectorList.forEach(System.out::println);

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

        final Set<Integer> parallelList = list.stream().parallel().collect(sampleCollector);
        parallelList.forEach(System.out::println);
    }
[実行結果]
2
1
0
3
10
================ parallel ================
combiner!
combiner!
combiner!
2
1
0
3
10

結果を見るとfinisherメソッドで追加した10が表示されているのがわかります。

また、並行処理(Stream#paralellを実行したもの)にした場合、
conbinerが呼び出されていることもわかります。

【まとめ】

今回はcollectメソッドで行われていることを調べてみました。

いろいろ書いたのですが基本的にはCollectorsクラス内に
定義されているメソッドを使用すると思います。