motoSEのブログ

SEの徒然備忘録&意思表示

Streamの終端処理 抜粋

Java8で追加されたStreamの終端処理で分からないのが結構あったので勉強してみました。

stream操作は、Stream生成⇒中間処理⇒終端処理の順番で実施します。 Stream生成⇒中間処理だけ実施しても、元のリスト等には影響を与えませんし、戻り値もありません。 何か処理をしたい場合、結果を得たい場合は必ず終端処理を実施しないといけません。

気が向けばもっと増やしていくと思いますが、今回は適当にピックアップしたものを挙げています。

count

そのままわかりやすいですが、Streamの件数をカウントします。 ふつうはfilter等の中間処理と合わせて使用します。

/**
 * Stream終端処理 countを確認
 */
@Test
public void count() {
    // そのまま
    long count = IntStream.rangeClosed(1, 10).count();
    System.out.println(count); // ⇒ 10
    // ふつうは中間処理と合わせて使用する
    long count2 = IntStream.rangeClosed(1, 10).filter(i -> i >  4).count();
    System.out.println(count2); // ⇒ 6
}

toArray

メソッド名そのままですがStreamを配列形式に変換して返却します。 toArrayには引数ありとなしの2パターン準備されていて、引数なしで実行した場合、Objectの配列として返却されます。 引数ありの場合、引数には引数として渡される整数のサイズの配列を返却する「generator」関数を指定する必要があります。

/**
 * Stream終端処理 toArrayを確認
 */
@Test
public void toArray() {
    List<String> testList = new ArrayList<String> (Arrays.asList("ABC1", "ABD2", "DBC3", "CBC4"));

    // 引数なしなのでObjectの配列で返却される
    Object[] streamToArray= testList.stream().toArray();
    System.out.println(Arrays.toString(streamToArray)); // ⇒ [ABC1, ABD2, DBC3, CBC4]

    // Stringの配列を返却するgenerator関数を指定すると返却した配列に値が設定されて返却されます。
    String[] streamToArrayFunc = testList.stream().toArray(String[]::new);
    System.out.println(Arrays.toString(streamToArrayFunc)); // ⇒ [ABC1, ABD2, DBC3, CBC4]

    // String[]::newは「i ->  new String[i]」の省略です。
    String[] streamToArrayFunc2 = testList.stream().toArray(i ->  new String[i]);
    System.out.println(Arrays.toString(streamToArrayFunc2)); // ⇒ [ABC1, ABD2, DBC3, CBC4]

    // 当然、指定されたサイズ以外のサイズの配列を返却するとエラーとなります。
    try {
        String[] streamToArrayFunc3 = testList.stream().toArray(i ->  new String[i - 1]);
    } catch (IllegalStateException e) {
        System.out.println(e.getClass().getName()); // ⇒ java.lang.IllegalStateException
    }
}

max, min

Streamの中から最大値、最小値を抽出します。 引数には、Comparatorを指定してください。 従来のようにcompareToで比較する関数を指定しても良いですが、Java8らしくComparator内の関数を利用するほうが分かりやすいのでおすすめです。 以下の例では、ComparatorにcomparingIntという関数を使用しています。 この関数は各要素を引数として受け取り、数値を返却して返却した値をもとに最小値を計算しています。

/**
 * Stream終端処理 max, minを確認
 */
@Test
public void maxMin() {
    List<String> testList = new ArrayList<String> (Arrays.asList("ABC1", "ABD2", "DBC3", "CBC4"));

    Optional<String> streamMax = testList.stream().max((str1, str2) -> str1.substring(3, 4).compareTo(str2.substring(3,4)));
    System.out.println(streamMax); ⇒ Optional[CBC4]

    Optional<String> streamMin = testList.stream().min(Comparator.comparingInt(str -> Integer.parseInt(str.substring(3,  4))));
    System.out.println(streamMin); ⇒ Optional[ABC1]
}

reduce

Streamの各要素に対してリダクション操作を実行して、その結果を返却します。 返却する値は、Streamの要素と同じ型になります。 リダクション操作ってなに?っていう人も多いと思います。実際私もそうでした。 簡単いうとリダクション操作とは、配列等の連続した値を受け取り、単一のサマリー結果を返却するものです。 いままで出てきた、count, max等がこれにあたります。 また、Javaでの以下のような操作もリダクション操作といえます。

int sum = 0;
for (int i : new int[]{1, 5, 8}) {
    sum += i;
}

reduceには引数の種類によって3種類の準備されています。

パラメータが1つ

List<String> testList = new ArrayList<String> (Arrays.asList("ABC1", "ABD2", "DBC3", "CBC4"));

Optional<String>  reduceValue1 = testList.stream().reduce((result, element) -> {
    // resultが今までの集計
    // elementが今回の値
    return String.join("/", result, element);
});
System.out.println(reduceValue1); // ⇒ Optional[ABC1/ABD2/DBC3/CBC4]

BinaryOperatorを引数に持つ場合、2つの値を引数にもつ関数を設定します。 第一引数は、前回この関数で返却した値が入ってきます。第二引数は、Streamの要素の値が入ってきます。 今回の場合では、初回はresult = ABC1, element = ABD2、2回目はresult = ABC1/ABD2, element = DBC3が入ってきます。

パラメータが2つ

String reduceValue2 = testList.stream().reduce("FIRST", (result, element) -> {
    // resultが今までの集計
    // elementが今回の値
    return String.join("/", str1, str2);
});
System.out.println(reduceValue2); // ⇒ FIRST/ABC1/ABD2/DBC3/CBC4

パラメータがひとつの場合とほぼ同じですが、こちらは第一引数に初期値を設定できます。 第一引数に指定した値に対して、第二引数に指定したアキュムレータが処理をします。 戻り値がOptinalでないのは、リストが空の場合でも第一引数に指定した値が返却されるからだと思います。

パラメータが3つ

String reduceValue3 = testList.parallelStream().reduce("FIRST", (result, element) -> {
    // resultが今までの集計
    // elementが今回の値
    return String.join("/", result, element);
}, (result1, result2) -> {
    // result1が今までの集計
    // result2が今回の値
    return String.join("/", result1, result2);
});
System.out.println(reduceValue3); // ⇒ FIRST/ABC1-FIRST/ABD2-FIRST/DBC3-FIRST/CBC4

パラメータが3つの場合は、パラメータが2つの場合と第一引数、第二引数は同じで、第三引数のみ違います。 第三引数は平行実行したときのみ呼び出される関数で、第二引数の結果を結合して返却します。

collect

Streamの各要素に対して可変リダクション操作を実行します。 リダクション操作とは、reduceのところで説明したとおりです。 では、可変とはなにか。 通常のリダクション操作では、単一の値を返却します。Stringとか、intとか。 可変リダクション操作とは、可変コンテナといわれるもの(CollectionとかStringBuilder等)に値を蓄積して返却します。 単一の値で返却するか、可変コンテナで返却するか返却するものの違いがリダクション操作と可変リダクション操作の違いです。

collect関数の説明に戻ります。 collect関数は、引数にCollectorを取る場合と、Supplier, accumulator, combinerの3つを取る場合の2種類存在します。 まずは、Collectorを取る場合です。

/**
 * Stream終端処理 collect Collectorを確認
 */
@Test
public void collect1() {
    List<String> testList = new ArrayList<String> (Arrays.asList("ABC1", "ABD2", "DBC3", "CBC4"));

    // Streamをリストに変換します。
    List<String> list = testList.stream().map(s -> s.substring(0,  2)).collect(Collectors.toList());
    System.out.println(list); // ⇒ [AB, AB, DB, CB]

    // groupingByで指定した関数が返却する値をキーにMapを返却します。
    Map<String, List<String>> groupingMap = testList.stream().collect(Collectors.groupingBy(s -> s.substring(0,1)));
    System.out.println(groupingMap); // ⇒ {A=[ABC1, ABD2], C=[CBC4], D=[DBC3]}
}

このように簡単に、可変リダクションを実現できます。 Collectorsには、便利な関数がたくさん用意されています。また、時間があればこちらも紹介したいと思います。

次に、Supplier, accumulator, combinerの3つを引数に取る場合です。

/**
 * Stream終端処理 collectを確認
 */
@Test
public void collect2() {
    List<String> testList = new ArrayList<String> (Arrays.asList("ABC1", "ABD2", "DBC3", "CBC4"));
    Map<String, List<String>> groupingMap2 = testList.stream().collect(HashMap<String, List<String>>::new, (map, s) -> {
        String key = s.substring(0, 1);
        List<String> innerList = map.getOrDefault(key, new ArrayList<String>());
        innerList.add(s);
        map.put(key, innerList);
    }, (map1, map2) -> {});
    System.out.println(groupingMap2); // ⇒ {A=[ABC1, ABD2], C=[CBC4], D=[DBC3]}

    Map<String, List<String>> groupingMap3 = testList.parallelStream().collect(HashMap<String, List<String>>::new, (map, s) -> {
        String key = s.substring(0, 1);
        List<String> innerList = map.getOrDefault(key, new ArrayList<String>());
        innerList.add(s);
        map.put(key, innerList);
    }, (map1, map2) -> {});
    System.out.println(groupingMap3); // ⇒ {A=[ABC1]} 値は変わる可能性があります。

    Map<String, List<String>> groupingMap4 = testList.parallelStream().collect(HashMap<String, List<String>>::new, (map, s) -> {
        String key = s.substring(0, 1);
        List<String> innerList = map.getOrDefault(key, new ArrayList<String>());
        innerList.add(s);
        map.put(key, innerList);
    }, (map1, map2) -> {
        map2.forEach((key, list) -> {
            List<String> innerList = map1.getOrDefault(key, new ArrayList<String>());
            innerList.addAll(list);
            map1.put(key, innerList);
        });
    });
    System.out.println(groupingMap4); // ⇒ {A=[ABC1, ABD2], C=[CBC4], D=[DBC3]}
}

さきほどのgroupingByを使った例と同じ値を返却します。 各引数は以下の意味を表します。 Supplier: collect関数で返却する可変コンテナを返却する関数を定義します。 accumulator: 第一引数にSupplierで返却したコンテナ、第二引数にStreamの各要素がわたってきます。 この関数で返却するコンテナに値を設定します。 combiner: 並列処理を実施しない場合、なにも設定しなくて良いです。 並列処理が行われたときに、第二引数で返却された値を結合するための処理を定義します。

以上で、終端処理の説明を終わります。 間違っているところがあれば教えてください。