技術と日常。

日々の気が付いたこと・気になったことを残しておきます。

[Java]StreamのgroupingByで、カテゴリごとの合計(Sum)を求める方法

前提

例えば以下のような、カテゴリとその量が定義されているクラスがあったとします。

public record Obj(
        String category,
        int amount
) {
}

そのリストから、各カテゴリごとの、量の集計処理を考えます。

// AとBはそれぞれいくつ?
List<Obj> objs = List.of(
        new Obj("A", 100),
        new Obj("A", 200),
        new Obj("B", 300),
        new Obj("B", 400),
        new Obj("B", 500)
);

結果がMap

int, long, doubleの場合

Collectors#groupingBy()と、Collectors#summingInt()を使用します。

objs.stream().collect(Collectors.groupingBy(
        Obj::category, // キーのマッピング
        Collectors.summingInt(Obj::amount) // オブジェクトから合計対象値のマッピング
));

> {A=300, B=1200}

summingDoublesummingLongも用意されているので、型にあわせて使用することができます。

それ以外の型の場合

それ以外の型の場合はCollectors#reducingにて処理ができます。

objs.stream().collect(Collectors.groupingBy(
        Obj::category,
        Collectors.reducing(
                0, // 初期値 今回はintの初期値なので0
                Obj::amount, // ↑でも出てきた、値のマッピング
                Integer::sum // それぞれの値に対して行う処理 今回はintの合計のためsum
        )
));

> {A=300, B=1200}

結果が元の型のCollection

上記の結果からのオブジェクトの生成も可能ですが、以下のような書き方はどうでしょうか。
この書き方であれば、集計対象が2つ以上になった場合も対応可能です。

objs.stream().collect(Collectors.groupingBy(
                Obj::category,
                Collectors.reducing((o1, o2) -> new Obj(o1.category(), o1.amount() + o2.amount()))
        )).values().stream().map(Optional::get).collect(Collectors.toSet());

> [Obj[category=A, amount=300], Obj[category=B, amount=1200]]

参考

Collectors (Java SE 17 & JDK 17)

Group by and sum objects like in SQL with Java lambdas? - Stack Overflow