Java Tips | コレクション:集計

Java Java
スポンサーリンク

集計は「一覧から“知りたい数字”だけを取り出す」技

集計は、ざっくり言うと
「たくさんのデータから、意味のある数字を取り出す」ことです。

売上一覧から「合計金額」「平均単価」「件数」を出す。
ユーザー一覧から「都道府県ごとの人数」を出す。
アクセスログから「日別のアクセス数」を出す。

こういう「一覧 → 数字」の変換を、
安全に・読みやすく・再利用しやすく書くための考え方とユーティリティが、
“コレクション集計”です。


一番基本:合計・平均・最大・最小・件数

Stream を使ったシンプルな数値集計

まずは、数値の List に対する基本的な集計から押さえましょう。

import java.util.List;

public class BasicAggregation {

    public static void main(String[] args) {
        List<Integer> prices = List.of(100, 200, 300);

        int sum = prices.stream()
                        .mapToInt(Integer::intValue)
                        .sum();

        double avg = prices.stream()
                           .mapToInt(Integer::intValue)
                           .average()
                           .orElse(0.0);

        int max = prices.stream()
                        .mapToInt(Integer::intValue)
                        .max()
                        .orElse(0);

        int min = prices.stream()
                        .mapToInt(Integer::intValue)
                        .min()
                        .orElse(0);

        long count = prices.stream()
                           .count();
    }
}
Java

ここでの重要ポイントは二つです。

一つ目は、mapToInt で「数値ストリーム」に変換してから集計していること。
sumaverage は「数値専用のメソッド」なので、
mapToInt / mapToLong / mapToDouble を通すのが基本パターンです。

二つ目は、average()max() が Optional を返すので、
orElse(0) のように「データが空だったときの値」を決めていること。
ここをユーティリティに閉じ込めておくと、呼び出し側が楽になります。


集計ユーティリティとしてまとめる

「毎回 Stream 書きたくない」を解消する

同じような集計を何度も書くなら、
ユーティリティメソッドにしてしまうとスッキリします。

import java.util.List;

public final class Aggregates {

    private Aggregates() {}

    public static int sumInt(List<Integer> source) {
        if (source == null || source.isEmpty()) {
            return 0;
        }
        return source.stream()
                     .mapToInt(Integer::intValue)
                     .sum();
    }

    public static double averageInt(List<Integer> source) {
        if (source == null || source.isEmpty()) {
            return 0.0;
        }
        return source.stream()
                     .mapToInt(Integer::intValue)
                     .average()
                     .orElse(0.0);
    }
}
Java

使い方はこうです。

List<Integer> prices = List.of(100, 200, 300);

int sum = Aggregates.sumInt(prices);
double avg = Aggregates.averageInt(prices);
Java

ここでの重要ポイントは、
「null や空リストの扱いをユーティリティ側で決めている」ことです。

呼び出し側は「とりあえず合計が欲しい」「平均が欲しい」とだけ考えればよく、
null や Optional の細かい扱いから解放されます。


オブジェクト一覧から「特定の項目」を集計する

例:商品一覧から「価格の合計」「平均価格」を出す

業務では、単なる List<Integer> よりも、
「オブジェクトの特定のフィールドを集計したい」ことがほとんどです。

class Item {
    private final String name;
    private final int price;
    public Item(String name, int price) {
        this.name = name;
        this.price = price;
    }
    public String getName() { return name; }
    public int getPrice() { return price; }
}
Java

これを集計するユーティリティは、こう書けます。

import java.util.List;
import java.util.function.ToIntFunction;

public final class Aggregates {

    private Aggregates() {}

    public static <T> int sumInt(List<T> source, ToIntFunction<? super T> mapper) {
        if (source == null || source.isEmpty()) {
            return 0;
        }
        return source.stream()
                     .mapToInt(mapper)
                     .sum();
    }

    public static <T> double averageInt(List<T> source, ToIntFunction<? super T> mapper) {
        if (source == null || source.isEmpty()) {
            return 0.0;
        }
        return source.stream()
                     .mapToInt(mapper)
                     .average()
                     .orElse(0.0);
    }
}
Java

使い方はこうです。

List<Item> items = List.of(
        new Item("A", 100),
        new Item("B", 200),
        new Item("C", 300)
);

int totalPrice = Aggregates.sumInt(items, Item::getPrice);
double avgPrice = Aggregates.averageInt(items, Item::getPrice);
Java

ここで深掘りしたい重要ポイントは三つです。

一つ目は、「ToIntFunction<? super T> mapper が“どの項目を集計するか”を表している」こと。
Item::getPrice を渡すことで、「価格を集計する」という意図が明確になります。

二つ目は、「ユーティリティ側は“どう集計するか”だけを知っていて、“何を集計するか”は呼び出し側が決める」構造になっていること。
これにより、同じメソッドで「価格」「数量」「ポイント」など、何でも集計できます。

三つ目は、「null や空リストの扱いを一箇所に閉じ込めている」こと。
プロジェクト全体で「空なら 0」「空なら 0.0」といったルールを統一できます。


グルーピングと組み合わせた「グループごとの集計」

例:カテゴリごとの合計金額

集計が本領を発揮するのは、「グルーピング」と組み合わせたときです。
例えば、商品を「カテゴリごとの合計金額」にしたい場合。

class Item {
    private final String category;
    private final int price;
    public Item(String category, int price) {
        this.category = category;
        this.price = price;
    }
    public String getCategory() { return category; }
    public int getPrice() { return price; }
}
Java

Stream だけで書くとこうなります。

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

List<Item> items = List.of(
        new Item("食品", 100),
        new Item("食品", 200),
        new Item("雑貨", 300)
);

Map<String, Integer> sumByCategory =
        items.stream()
             .collect(Collectors.groupingBy(
                     Item::getCategory,
                     Collectors.summingInt(Item::getPrice)
             ));
Java

ここでの重要ポイントは、
groupingBy(カテゴリ, summingInt(価格)) という一行が、
“カテゴリごとに価格を合計する”という業務仕様をそのまま表している」ことです。

グルーピングと集計を一気に書けるので、
「まず Map に詰めてから for で回して…」という手作業より、
はるかに読みやすく・バグも入りにくくなります。


集計結果を「読みやすい形」にラップする

合計・平均・最大・最小をまとめて返す

「合計だけ」「平均だけ」ではなく、
「合計・平均・最大・最小・件数をまとめて欲しい」ことも多いです。

その場合は、結果をラップする小さなクラスを用意すると、
呼び出し側がとても扱いやすくなります。

public final class IntSummary {

    private final long count;
    private final int sum;
    private final int min;
    private final int max;
    private final double average;

    public IntSummary(long count, int sum, int min, int max, double average) {
        this.count = count;
        this.sum = sum;
        this.min = min;
        this.max = max;
        this.average = average;
    }

    public long getCount() { return count; }
    public int getSum() { return sum; }
    public int getMin() { return min; }
    public int getMax() { return max; }
    public double getAverage() { return average; }
}
Java

ユーティリティ側はこうです。

import java.util.IntSummaryStatistics;
import java.util.List;
import java.util.function.ToIntFunction;

public final class Aggregates {

    private Aggregates() {}

    public static <T> IntSummary summarizeInt(List<T> source, ToIntFunction<? super T> mapper) {
        if (source == null || source.isEmpty()) {
            return new IntSummary(0, 0, 0, 0, 0.0);
        }
        IntSummaryStatistics stats =
                source.stream()
                      .mapToInt(mapper)
                      .summaryStatistics();

        return new IntSummary(
                stats.getCount(),
                (int) stats.getSum(),
                stats.getMin(),
                stats.getMax(),
                stats.getAverage()
        );
    }
}
Java

使い方はこうです。

IntSummary summary = Aggregates.summarizeInt(items, Item::getPrice);

System.out.println(summary.getSum());
System.out.println(summary.getAverage());
System.out.println(summary.getMax());
Java

ここでの重要ポイントは、
「集計結果を“意味のあるまとまり”として返している」ことです。

バラバラの数値ではなく、
「これは価格の集計結果だ」と分かる形で扱えるので、
コードの意図がぐっと読みやすくなります。


null を含むデータを集計するときの考え方

「null は 0 とみなすか」「そもそも除外するか」を決める

現実のデータには null が混ざります。
集計するときに重要なのは、
「null をどう扱うか」をプロジェクトとして決めることです。

例えば、「価格が null の商品は集計から除外する」なら、こう書きます。

public static <T> int sumIntIgnoreNull(List<T> source, ToIntFunction<? super T> mapper, java.util.function.Predicate<? super T> nonNullPredicate) {
    if (source == null || source.isEmpty()) {
        return 0;
    }
    return source.stream()
                 .filter(nonNullPredicate)
                 .mapToInt(mapper)
                 .sum();
}
Java

あるいは、「null は 0 とみなす」なら、
mapper の中で 0 に変換してしまう、という設計もあります。

ここでのポイントは、
「null の扱いも“集計ルールの一部”」だと意識することです。

ユーティリティに閉じ込めておけば、
呼び出し側は「この集計は null をどう扱うのか」を意識せずに済みます。


まとめ:集計ユーティリティで身につけてほしい感覚

コレクションの集計は、
単に「sum を呼ぶテクニック」ではなく、
「一覧から“知りたい数字”を、安全に・一貫して取り出すための設計」です。

数値の List に対する基本的な sum / average / max / min / count を押さえる。
オブジェクト一覧に対しては、「どの項目を集計するか」を関数(Item::getPrice など)で渡す。
グルーピングと組み合わせて、「カテゴリごとの合計」「ステータスごとの件数」などを一行で書けるようにする。
よく使う集計はユーティリティにまとめ、null や空データの扱いを一箇所に閉じ込める。
必要なら「集計結果クラス」を用意して、合計・平均・最大・最小・件数をひとまとまりで扱う。

もしあなたのコードのどこかに、
同じような for 文で合計や件数を手作業で計算している部分があれば、
それを一度「集計ユーティリティ+Stream ベース」に置き換えられないか眺めてみてください。

その小さな整理が、
「一覧から“意味のある数字”を、迷いなく取り出せるエンジニア」への、
確かな一歩になります。

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