🌐

[Java]Streamでの文字列結合・集計・グループ分けのCollectorsを使った簡単な説明とサンプル

に公開

Java 11で利用可能なjava.util.stream.Collectorsには、コレクション操作を簡潔にするための便利なメソッドがいくつかあり、これらを使って書くと読みやすく書けます。
本記事ではその中から、Collectors.toList(), Collectors.joining(), Collectors.summingInt(), Collectors.groupingBy(), Collectors.partitioningBy()といった比較的分かりやすい5つのメソッドを紹介し、それぞれの使用例をお伝えします。

Collectors.toList()

このメソッドは、ストリームの要素からリストを作成します。
既によく使われている方も多いかと思います。filterした結果やmapした結果をリストに戻すときによく使うやつですね。

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class CollectorsExample {
    public static void main(String[] args) {
        List<String> list = Stream.of("apple", "banana", "cherry")
                                  .collect(Collectors.toList());
        System.out.println(list);  // [apple, banana, cherry]
    }
}

Collectors.joining()

このメソッドは、ストリームの要素を一つの文字列に結合します。区切り文字(下の例で言う "," )やプレフィックス(下の例で言う "[" )、サフィックス(下の例で言う "]" )も指定可能です。
引数に何も指定しないパターン(各文字列がそのまま連結される)、区切り文字だけ指定するパターン、区切り文字・プレフィックス・サフィックスを指定するパターンの3通りがオーバーロードされています。

import java.util.stream.Collectors;
import java.util.stream.Stream;

public class CollectorsExample {
    public static void main(String[] args) {
        String result = Stream.of("apple", "banana", "cherry")
                              .collect(Collectors.joining(", ", "[", "]"));
        System.out.println(result);  // [apple, banana, cherry]
    }
}

Collectors.summingInt()

このメソッドは、ストリームの要素の特定のプロパティの合計を計算します。
以下の例はIntegerのStreamなのでintValueプロパティを指定していますが、例えばMemberクラスのインスタンスにintを返すgetAge()メソッドがある場合は .summingInt(Member::getAge) といったような書き方でMemberのAgeを合計できます。もちろん .summingInt(member -> member.getAge()) といったようにラムダ式も書けます。
他には、averagingIntという平均を求める版もありますし、さらにLong、Double版もあります。

import java.util.List;
import java.util.stream.Collectors;

public class CollectorsExample {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5);
        int sum = numbers.stream()
                         .collect(Collectors.summingInt(Integer::intValue));
        System.out.println(sum);  // 15
    }
}

ただし、IntStreamというint用のStreamのようなものが別に存在し、 sum() / average() といった同様のメソッドが用意されています。ただしIntStreamになっていない場合、前もって Stream#mapToInt() を利用しIntStreamに変換しておく必要があります。
mapToInt() ↓
https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/util/stream/Stream.html#mapToInt(java.util.function.ToIntFunction)
使用例は以下の通りです。String::lengthとなっているところはラムダ式でもOKです。

import java.util.List;
import java.util.stream.Collectors;

public class IntStreamExample {
    public static void main(String[] args) {
        // リストの作成
        List<String> strings = List.of("apple", "banana", "cherry");

        // mapToIntを使用して各文字列の長さを取得し、合計を計算
        int sumOfLengths = strings.stream()
                                  .mapToInt(String::length)
                                  .sum();

        // 結果を出力
        System.out.println("The sum of lengths is: " + sumOfLengths);  // The sum of lengths is: 17
    }
}

Collectors.groupingBy()

このメソッドは、ストリームの要素を特定の基準に基づいてグループ化します。
引数に渡すラムダ式が返す値が同じものを集めて1つのマップエントリのValueにします。以下の例はitemsリストのそれぞれの文字列の最初の文字( item->item.charAt(0) )でグループ化しています。頭文字aとbとcの3つのグループに分けられています。
実際の使用シーンは地域や範囲などでグループ化することが多いでしょう。

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class CollectorsExample {
    public static void main(String[] args) {
        List<String> items = List.of("apple", "banana", "cherry", "apricot", "blueberry");
        Map<Character, List<String>> groupedByFirstLetter = items.stream()
                                                                  .collect(Collectors.groupingBy(item -> item.charAt(0)));
        System.out.println(groupedByFirstLetter);  // {a=[apple, apricot], b=[banana, blueberry], c=[cherry]}
    }
}

Streamを使わない場合

ちなみに、Streamを使わずに書くと以下の通りとなります。
頭文字のリストを初期化する処理が必要になっています。Stream.collectを使うとこうした本質的ではない処理を省けます。これらを書き忘れてバグを生むことも多いかと思います。

import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;

public class CollectorsExample {
    public static void main(String[] args) {
        List<String> items = List.of("apple", "banana", "cherry", "apricot", "blueberry");

        // アイテムを最初の文字でグループ化するためのマップを作成
        Map<Character, List<String>> groupedByFirstLetter = new HashMap<>();

        // 各アイテムをループしてグループ化
        for (String item : items) {
            char firstLetter = item.charAt(0);
            // グループが存在しない場合、新しいリストを作成
            groupedByFirstLetter.putIfAbsent(firstLetter, new ArrayList<>());
            // アイテムを対応するリストに追加
            groupedByFirstLetter.get(firstLetter).add(item);
        }

        // 結果を出力
        System.out.println(groupedByFirstLetter);  // {a=[apple, apricot], b=[banana, blueberry], c=[cherry]}
    }
}

Collectors.partitioningBy()

このメソッドは、ストリームの要素をブールのラムダ式に基づいて2つのグループに分割します。
以下の例では偶数か判定するラムダ式を渡して、分割した結果をMapに格納しています。

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class CollectorsExample {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
        Map<Boolean, List<Integer>> partitionedByEven = numbers.stream()
                                                               .collect(Collectors.partitioningBy(num -> num % 2 == 0));
        System.out.println(partitionedByEven);  // {false=[1, 3, 5], true=[2, 4, 6]}
    }
}

これらのメソッドを使うことで、ストリームの操作がより直感的かつ効率的になり、コードを書いた意図と実際の実装がより近づけられると思います。
Java 11のCollectorsを活用してコレクションの操作を簡素化していきましょう!

FYI: https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/util/stream/Collectors.html

Discussion