たとえば、ユーザとそのユーザが支払った金額が以下のような形で与えられるとします。
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
ただここまでくると、黒魔術感がただよいはじめるので、ほどほどにしたい(´・ω・`)