Java

StreamでGroup By処理を行いたい(group-by-count, group-by-sum, group-by-max)

たとえば、ユーザとそのユーザが支払った金額が以下のような形で与えられるとします。

public class Payment {

    public static void main(String[] args) {
        var payments = List.of(
            new Payment("A", 10),
            new Payment("B", 20),
            new Payment("B", 30),
            new Payment("C", 40),
            new Payment("C", 50),
            new Payment("C", 60)
        );
    }

    private String name;
    private int value;

    public Payment(String name, int value) {
        this.name = name;
        this.value = value;
    }
    public String getName() { return name; }
    public int getValue() { return value; }
}

では各々のユーザについて、支払いの個数を支払いの回数や支払った金額の合計、あるいは金額の最大値を求めたい。SQLであればGROUP BYとウィンドウ関数の組み合わせで簡単に求めることができそうですが、JavaのStreamではどのように書くべきでしょうか?

select name, count(*) from payment group by name;
select name, sum(value) from payment group by name;
select name, max(value) from payment group by name;

全体的な方針としてはCollectors.groupingByを利用します。まず支払いの回数、つまりgroup-by-count

var counts = payments.stream().collect(Collectors.groupingBy(Payment::getName, Collectors.counting()));
counts.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).forEach(System.out::println);
// A=1
// B=2
// C=3

Collectors.countingというその名もずばりなメソッドが用意されているので、これを使うのがよさそう。次に支払った金額の合計。要はgroup-by-sumですが、これもCollectors.summingIntというわかりやすい名前のメソッドがあるので、これを利用するだけです。

var sums = payments.stream().collect(Collectors.groupingBy(Payment::getName, Collectors.summingInt(Payment::getValue)));
sums.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).forEach(System.out::println);
// A=10
// B=50
// C=150

最後に「支払った金額の最大値」=group-by-maxですが、個人的にはもっとも議論の余地があるように感じます。基本的な方針としてはCollectors.maxByを利用するのが手っ取り早そうです。

var maxs = payments.stream().collect(Collectors.groupingBy(Payment::getName, Collectors.maxBy(Comparator.comparingInt(Payment::getValue))));
maxs.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue().get().getValue()).forEach(System.out::println);
// A=10
// B=30
// C=60

このとき変数maxsの型はMap<String, Optional<Payment>>です。Optionalは「nullかもしれない」ことを注意喚起するマーカのようなものですが、ここではビジネスロジック上、maxsのvalueがnullになることはありえません。要はここでのOptionalはあまり意味がないので、取っ払いたい。言い換えればmaxsの型をMap<String, Payment>にしたいわけですが、このような場合は次のようにするのが手っ取り早そうです。

var maxs = payments.stream().collect(Collectors.groupingBy(Payment::getName, Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparing(Payment::getValue)), Optional::get)));
maxs.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue().getValue()).forEach(System.out::println);
// A=10
// B=30
// C=60

ただここまでくると、黒魔術感がただよいはじめるので、ほどほどにしたい(´・ω・`)