Java Stream APIのCollectionsユースケースまとめ

n-ozawan

皆さん、こんにちは。LP開発グループのn-ozawanです。
12月頃から冬眠に入る熊ですが、同じクマ科のパンダは冬眠しません。パンダが食べる笹は一年中手に入るので、冬眠(エネルギーを節約)する必要がありません。また、笹は低カロリーなので冬眠に必要なエネルギーを確保できないのも理由の1つです。

本題です。
Stream APIのcollect()は、単にStreamをCollection型に変換してくれるだけでなく、集計やグルーピングなど多くの用途に対応しています。今回はそんなCollectionsクラスで提供されている、割と使うユースケースをまとめます。

Collections ユースケース

Collectionに変換

StreamをCollection型に変換します。最も基本的な使い方です。

String[] names = {"佐藤", "鈴木", "高橋", "田中", "伊藤", "伊藤"};

// List型に変換
List<String> nameList = Arrays.stream(names).collect(Collectors.toList());
IO.println(nameList);   // [佐藤, 鈴木, 高橋, 田中, 伊藤, 伊藤]

// Set型に変換
Set<String> nameSet = Arrays.stream(names).collect(Collectors.toSet());
IO.println(nameSet);    // [佐藤, 鈴木, 高橋, 田中, 伊藤]

// 任意のCollection型に変換
Deque<String> nameDeque = Arrays.stream(names).collect(Collectors.toCollection(ArrayDeque::new));
IO.println(nameDeque);  // [佐藤, 鈴木, 高橋, 田中, 伊藤, 伊藤]

Map型に変換

StreamをMap型に変換します。IDなどの一意キーで、オブジェクトに効率よくアクセスしたい場合に利用します。

Person[] people = {
  new Person("ID1", "佐藤"),
  new Person("ID2", "鈴木"),
  new Person("ID3", "高橋"),
  new Person("ID4", "田中"),
  new Person("ID5", "伊藤")
};

// IDをキー、Personオブジェクトを値とするMapに変換
Map<String, Person> idToPersonMap = Arrays.stream(people)
  .collect(Collectors.toMap(Person::getId, person -> person));
IO.println(idToPersonMap);  // {ID1=Person@1a2b3c4, ID2=Person@5d6e7f8, ID3=Person@9a0b1c2, ID4=Person@3d4e5f6, ID5=Person@7a8b9c0}

// IDをキー、名前を値とするMapに変換
Map<String, String> idToNameMap = Arrays.stream(people)
  .collect(Collectors.toMap(Person::getId, Person::getName));
IO.println(idToNameMap);  // {ID1=佐藤, ID2=鈴木, ID3=高橋, ID4=田中, ID5=伊藤}

文字列結合

文字列を結合します。文字列以外のStreamを結合しようとするとエラーになりますので、map()などで文字列のStreamにしてから使います。

Person[] people = {
  new Person("佐藤", 28),
  new Person("鈴木", 34),
  new Person("高橋", 22),
  new Person("田中", 40),
  new Person("伊藤", 30)
};

// 区切り文字なしで連結
String result1 = Arrays.stream(people).map(Person::getName)
  .collect(Collectors.joining());
IO.println(result1);  // 佐藤鈴木高橋田中伊藤

// 区切り文字ありで連結
String result2 = Arrays.stream(people).map(Person::getName)
  .collect(Collectors.joining(", "));
IO.println(result2);  // 佐藤, 鈴木, 高橋, 田中, 伊藤

// 接頭辞・接尾辞ありで連結
String result3 = Arrays.stream(people).map(Person::getName)
  .collect(Collectors.joining(", ", "★開始★, ", ", ★終了★"));
IO.println(result3);  // ★開始★, 佐藤, 鈴木, 高橋, 田中, 伊藤, ★終了★

数値集計・統計

数値の合計や平均などを求めます。

Person[] people = {
  new Person("佐藤", 28),
  new Person("鈴木", 34),
  new Person("高橋", 22),
  new Person("田中", 40),
  new Person("伊藤", 30)
};

// 平均年齢
double averageAge = Arrays.stream(people)
  .collect(Collectors.averagingInt(Person::getAge));
IO.println(averageAge);  // 30.8

// 最年長
int maxAge = Arrays.stream(people)
  .collect(Collectors.summarizingInt(Person::getAge))
  .getMax();
IO.println(maxAge);      // 40

// 一括集計 (個数、合計、最小、平均、最大)
IntSummaryStatistics ageStats = Arrays.stream(people)
  .collect(Collectors.summarizingInt(Person::getAge));
IO.println(ageStats);    // IntSummaryStatistics{count=5, sum=154, min=22, average=30.800000, max=40

グルーピング

指定された要素ごとにグルーピングして、Map型として返却します。以下のソースコードは部署ごとにグルーピングした例です。Collectors.groupingBy()の第2引数に、先ほどの数値集計や統計を利用することで、部署ごとに計算することもできます。

Person[] people = {
  new Person("佐藤", 28, "営業"),
  new Person("鈴木", 34, "開発"),
  new Person("高橋", 22, "営業"),
  new Person("田中", 40, "開発"),
  new Person("伊藤", 30, "総務")
};

// 部署ごとにグルーピング
Map<String, List<Person>> groupedByDepartment = Arrays.stream(people)
  .collect(Collectors.groupingBy(Person::getDepartment));
IO.println(groupedByDepartment);          // {営業=[Person@1a2b3c4, Person@5d6e7f8], 開発=[Person@9a0b1c2, Person@3d4e5f6], 総務=[Person@7a8b9c0]}
IO.println(groupedByDepartment.get("営業").get(0).getName());  // 佐藤

// 部署ごとに年齢の平均を算出
Map<String, Double> averageAgeByDepartment = Arrays.stream(people)  
  .collect(Collectors.groupingBy(
    Person::getDepartment,
    Collectors.averagingInt(Person::getAge)
  ));
IO.println(averageAgeByDepartment);       // {営業=25.0, 開発=37.0, 総務=30.0}

// 部署ごとに最年長者を取得
Map<String, Optional<Person>> oldestByDepartment = Arrays.stream(people)
  .collect(Collectors.groupingBy(
    Person::getDepartment,
    Collectors.maxBy(Comparator.comparingInt(Person::getAge))
  ));
IO.println(oldestByDepartment.get("開発").get().getName());  // 田中

マッピング、変換

Collectors.mapping()Collectors.collectingAndThen()を使うことで、値や型変換をすることができます。これら単体で見ると、Stream.map()と同じように見えますが、Collectors.groupingBy()と合わせることで強力な処理ができるようになります。

Person[] people = {
  new Person("佐藤", 28, "営業"),
  new Person("鈴木", 34, "開発"),
  new Person("高橋", 22, "営業"),
  new Person("田中", 40, "開発"),
  new Person("伊藤", 30, "総務")
};

// 部署ごとに名前のリストを取得
Map<String, List<String>> namesByDepartment = Arrays.stream(people)
  .collect(Collectors.groupingBy(
    Person::getDepartment,
    Collectors.mapping(Person::getName, Collectors.toList())
  ));
IO.println(namesByDepartment);  // {営業=[佐藤, 高橋], 開発=[鈴木, 田中], 総務=[伊藤]}

// 部署ごとに名前をカンマ区切りでグルーピング
Map<String, String> namesJoinedByDepartment = Arrays.stream(people) 
  .collect(Collectors.groupingBy(
    Person::getDepartment,
    Collectors.collectingAndThen(
      Collectors.mapping(Person::getName, Collectors.toList()),
      names -> String.join(", ", names)
    )
  ));
IO.println(namesJoinedByDepartment);  // {営業=佐藤, 高橋, 開発=鈴木, 田中, 総務=伊藤}

おわりに

グルーピングなどは集計処理などで使うことがあります。宣言的に記述することができるので、複雑な集計処理でも可読性高く実装できるのが魅力ですね。

ではまた。

Recommendおすすめブログ