【プログラミングしてみよう54】JavaのStream APIを深める(reduce・collect・groupingBy)

Java

前回はExecutorServiceを使ったスレッドプールによる効率的な並行処理を学びました。

今回は「Stream API」をさらに深掘りします。 以前 filter・map・collect の基本を学びましたが、今回は reduce・groupingBy・partitioningBy など、より高度な集計・グループ化の操作を解説します。

Stream APIの復習と今回の内容

以前学んだStream APIの基本操作を振り返ります。

list.stream()
    .filter(x -> x > 0)     // 絞り込み
    .map(x -> x * 2)         // 変換
    .collect(Collectors.toList()); // リストに収集

今回はさらに以下を学びます。

reduce()         // 要素をたたみ込んで1つの値にまとめる
Collectors.groupingBy()    // 条件でグループ分けする
Collectors.partitioningBy() // 2グループに分ける
Collectors.counting()      // グループの件数を数える
Collectors.joining()       // 文字列を結合する
Collectors.summarizingInt() // 合計・平均・最大・最小をまとめて取得

サンプルコード(reduceで集計する)

ファイル作成

「Java42」フォルダに ReduceTest.java という名前でファイルを作成します

コード入力

以下のコードを入力します。

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

public class ReduceTest {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // 合計(reduceで実装)
        int sum = numbers.stream()
                         .reduce(0, (a, b) -> a + b);
        System.out.println("合計: " + sum);

        // 合計(mapToIntとsumで書くとシンプル)
        int sum2 = numbers.stream()
                          .mapToInt(Integer::intValue)
                          .sum();
        System.out.println("合計(mapToInt): " + sum2);

        // 最大値
        Optional<Integer> max = numbers.stream()
                                       .reduce((a, b) -> a > b ? a : b);
        System.out.println("最大値: " + max.orElse(0));

        // 文字列の連結
        List<String> words = List.of("Java", "は", "楽しい");
        String sentence = words.stream()
                               .reduce("", (a, b) -> a + b);
        System.out.println("連結: " + sentence);

        // 積(掛け算)
        int product = numbers.stream()
                             .reduce(1, (a, b) -> a * b);
        System.out.println("積: " + product);

        // IntStreamの便利メソッド
        var stats = numbers.stream()
                           .mapToInt(Integer::intValue)
                           .summaryStatistics();
        System.out.println("\n--- 統計情報 ---");
        System.out.println("件数: " + stats.getCount());
        System.out.println("合計: " + stats.getSum());
        System.out.println("平均: " + stats.getAverage());
        System.out.println("最小: " + stats.getMin());
        System.out.println("最大: " + stats.getMax());
    }
}

コンパイル

javac ReduceTest.java

実行

java ReduceTest

実行結果は次のようになります。

合計: 55
合計(mapToInt): 55
最大値: 10
連結: Javaは楽しい
積: 3628800

--- 統計情報 ---
件数: 10
合計: 55
平均: 5.5
最小: 1
最大: 10

サンプルコード(collectorsを使った高度な集計)

ファイル作成

「Java42」フォルダに CollectorsTest.java という名前でファイルを作成します

コード入力

以下のコードを入力します。

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

public class CollectorsTest {
    record Student(String name, String grade, int score) {}

    public static void main(String[] args) {
        var students = List.of(
            new Student("ぱんまる", "A", 90),
            new Student("たろう",   "B", 75),
            new Student("はなこ",   "A", 88),
            new Student("けんじ",   "C", 60),
            new Student("あいこ",   "B", 82),
            new Student("ゆうき",   "C", 55),
            new Student("みほ",     "A", 95)
        );

        // groupingBy: グレードでグループ分け
        Map<String, List<Student>> byGrade = students.stream()
                .collect(Collectors.groupingBy(Student::grade));

        System.out.println("--- グレード別グループ ---");
        byGrade.forEach((grade, list) -> {
            System.out.print(grade + "グループ: ");
            System.out.println(list.stream()
                    .map(Student::name)
                    .collect(Collectors.joining(", ")));
        });

        // groupingBy + counting: グレード別の人数
        Map<String, Long> countByGrade = students.stream()
                .collect(Collectors.groupingBy(Student::grade, Collectors.counting()));

        System.out.println("\n--- グレード別人数 ---");
        countByGrade.forEach((grade, count) ->
                System.out.println(grade + ": " + count + "人"));

        // groupingBy + averagingInt: グレード別の平均点
        Map<String, Double> avgByGrade = students.stream()
                .collect(Collectors.groupingBy(Student::grade,
                         Collectors.averagingInt(Student::score)));

        System.out.println("\n--- グレード別平均点 ---");
        avgByGrade.forEach((grade, avg) ->
                System.out.printf("%s: %.1f点%n", grade, avg));

        // partitioningBy: 80点以上かどうかで2グループに分ける
        Map<Boolean, List<Student>> partition = students.stream()
                .collect(Collectors.partitioningBy(s -> s.score() >= 80));

        System.out.println("\n--- 80点以上/未満 ---");
        System.out.print("80点以上: ");
        System.out.println(partition.get(true).stream()
                .map(Student::name)
                .collect(Collectors.joining(", ")));
        System.out.print("80点未満: ");
        System.out.println(partition.get(false).stream()
                .map(Student::name)
                .collect(Collectors.joining(", ")));

        // joining: 名前をカンマ区切りで結合
        String allNames = students.stream()
                .map(Student::name)
                .collect(Collectors.joining(", ", "[", "]"));
        System.out.println("\n全員: " + allNames);

        // summarizingInt: 点数の統計をまとめて取得
        var scoreSummary = students.stream()
                .collect(Collectors.summarizingInt(Student::score));
        System.out.println("\n--- 点数統計 ---");
        System.out.println("人数: " + scoreSummary.getCount());
        System.out.println("合計: " + scoreSummary.getSum());
        System.out.printf("平均: %.1f%n", scoreSummary.getAverage());
        System.out.println("最高: " + scoreSummary.getMax());
        System.out.println("最低: " + scoreSummary.getMin());
    }
}

コンパイル

javac CollectorsTest.java

実行

java CollectorsTest

実行結果は次のようになります(グループの順番は異なる場合があります)。

--- グレード別グループ ---
Aグループ: ぱんまる, はなこ, みほ
Bグループ: たろう, あいこ
Cグループ: けんじ, ゆうき

--- グレード別人数 ---
A: 3人
B: 2人
C: 2人

--- グレード別平均点 ---
A: 91.0点
B: 78.5点
C: 57.5点

--- 80点以上/未満 ---
80点以上: ぱんまる, はなこ, あいこ, みほ
80点未満: たろう, けんじ, ゆうき

全員: [ぱんまる, たろう, はなこ, けんじ, あいこ, ゆうき, みほ]

--- 点数統計 ---
人数: 7
合計: 545
平均: 77.9
最高: 95
最低: 55

コードの解説

reduce(初期値, (a, b) -> 処理) は要素を左から順に「たたみ込む」操作です。reduce(0, (a, b) -> a + b) は最初に 0 + 1 = 1、次に 1 + 2 = 3、次に 3 + 3 = 6…と順番に計算して最終的に合計を求めます。mapToInt().sum() や mapToInt().max() の方がシンプルに書けるため、合計・最大・最小は専用メソッドを使う方が実践的です。

summaryStatistics() は件数・合計・平均・最小・最大をまとめて取得できる便利なメソッドです。複数の統計値が必要な場合にまとめて取れます。

Collectors.groupingBy(Student::grade) はグレードの値でグループ分けし、Map<String, List<Student>> として返します。第2引数に Collectors.counting() や Collectors.averagingInt() を渡すと、グループごとの件数や平均も同時に求められます。

Collectors.partitioningBy(条件) は条件が true か false かで2グループに分け、Map<Boolean, List<T>> として返します。合格・不合格の分類など「2択で分けたい」場面で groupingBy より明確に意図を表現できます。

Collectors.joining(区切り文字, 先頭, 末尾) は文字列を結合するCollectorです。joining(", ", "[", "]") とすると [ぱんまる, たろう, ...] のように前後を括弧で囲んだ形にできます。

まとめ

Stream APIの reduce で要素をたたみ込んで1つの値にまとめ、groupingBy でグループ分け、partitioningBy で2グループに分け、joining で文字列結合、summarizingInt で統計情報をまとめて取得できます。これらを組み合わせることで、ループとMapを駆使していた複雑な集計処理をシンプルに書けるようになります。Stream APIを使いこなすことで、Javaでのデータ処理が一段と洗練されます。

2026/08/02(日) ぱんまる

コメント

タイトルとURLをコピーしました