リストfooListがあります
class Foo {
private String category;
private int amount;
private int price;
... constructor, getters & setters
}
カテゴリー別にグループ化し、金額と価格を合計したいと思います。
結果はマップに保存されます:
Map<Foo, List<Foo>> map = new HashMap<>();
キーは、同じカテゴリのすべてのオブジェクトの値としてのリストとともに、合計金額と価格を保持するFooです。
これまでのところ、私は次のことを試しました:
Map<String, List<Foo>> map = fooList.stream().collect(groupingBy(Foo::getCategory()));
これで、Stringキーを、集計された金額と価格を保持するFooオブジェクトに置き換えるだけで済みます。私が立ち往生しているところです。私はこれを行う方法を見つけることができないようです。
少しいですが、動作するはずです:
list.stream().collect(Collectors.groupingBy(Foo::getCategory))
.entrySet().stream()
.collect(Collectors.toMap(x -> {
int sumAmount = x.getValue().stream().mapToInt(Foo::getAmount).sum();
int sumPrice= x.getValue().stream().mapToInt(Foo::getPrice).sum();
return new Foo(x.getKey(), sumAmount, sumPrice);
}, Map.Entry::getValue));
Sweepers answer の私のバリエーションでは、個々のフィールドを合計するために2回ストリーミングする代わりに、縮小コレクターを使用します。
Map<Foo, List<Foo>> map = fooList.stream()
.collect(Collectors.groupingBy(Foo::getCategory))
.entrySet().stream()
.collect(Collectors.toMap(e -> e.getValue().stream().collect(
Collectors.reducing((l, r) -> new Foo(l.getCategory(),
l.getAmount() + r.getAmount(),
l.getPrice() + r.getPrice())))
.get(),
e -> e.getValue()));
しかし、多くの短命のFoos
を作成するため、実際にはbetterではありません。
ただし、Foo
は、結果のhashCode
に対してequals
のみを考慮するcategory
-およびmap
-の実装を提供するために必要であることに注意してください。正しく動作します。これは、おそらくFoo
sに必要なものではないでしょう。個別のFooSummary
クラスを定義して、集計データを含めることをお勧めします。
次のように、特別な専用コンストラクタと、hashCode
に一貫して実装されたequals
およびFoo
メソッドがある場合:
public Foo(Foo that) { // not a copy constructor!!!
this.category = that.category;
this.amount = 0;
this.price = 0;
}
public int hashCode() {
return Objects.hashCode(category);
}
public boolean equals(Object another) {
if (another == this) return true;
if (!(another instanceof Foo)) return false;
Foo that = (Foo) another;
return Objects.equals(this.category, that.category);
}
上記のhashCode
およびequals
の実装により、マップ内で意味のあるキーとしてFoo
を使用できます(そうしないとマップが破損します)。
これで、Foo
属性とamount
属性の集計を同時に実行するprice
の新しいメソッドの助けを借りて、2つのステップで必要なことができます。最初の方法:
public void aggregate(Foo that) {
this.amount += that.amount;
this.price += that.price;
}
最後の解決策:
Map<Foo, List<Foo>> result = fooList.stream().collect(
Collectors.collectingAndThen(
Collectors.groupingBy(Foo::new), // works: special ctor, hashCode & equals
m -> { m.forEach((k, v) -> v.forEach(k::aggregate)); return m; }));
編集:いくつかの観察が欠落していました...
一方では、このソリューションでは、同じhashCode
に属している場合、2つの異なるequals
インスタンスを等しいと見なすFoo
およびcategory
の実装を使用する必要があります。たぶんこれは望ましくないか、より多くの属性または他の属性を考慮する実装が既にあります。
一方、属性の1つでインスタンスをグループ化するために使用されるマップのキーとしてFoo
を使用することは、非常に一般的なユースケースです。 category
属性を使用してカテゴリごとにグループ化し、グループを保持するMap<String, List<Foo>>
と、集約されたprice
を保持するMap<String, Foo>
とamount
。どちらの場合もキーはcategory
です。
これに加えて、このソリューションは、エントリが入力された後にマップのキーを変更します。これはマップを壊す可能性があるため、危険です。ただし、ここでは、Foo
もhashCode
equals
の実装にも参加しないFoo
の属性のみを変更しています。要件の異常性のため、この場合、このリスクは許容できると思います。
量と価格を保持するヘルパークラスを作成することをお勧めします
final class Pair {
final int amount;
final int price;
Pair(int amount, int price) {
this.amount = amount;
this.price = price;
}
}
次に、リストを収集してマップします。
List<Foo> list =//....;
Map<Foo, Pair> categotyPrise = list.stream().collect(Collectors.toMap(foo -> foo,
foo -> new Pair(foo.getAmount(), foo.getPrice()),
(o, n) -> new Pair(o.amount + n.amount, o.price + n.price)));
私の解決策:)
public static void main(String[] args) {
List<Foo> foos = new ArrayList<>();
foos.add(new Foo("A", 1, 10));
foos.add(new Foo("A", 2, 10));
foos.add(new Foo("A", 3, 10));
foos.add(new Foo("B", 1, 10));
foos.add(new Foo("C", 1, 10));
foos.add(new Foo("C", 5, 10));
List<Foo> summarized = new ArrayList<>();
Map<Foo, List<Foo>> collect = foos.stream().collect(Collectors.groupingBy(new Function<Foo, Foo>() {
@Override
public Foo apply(Foo t) {
Optional<Foo> fOpt = summarized.stream().filter(e -> e.getCategory().equals(t.getCategory())).findFirst();
Foo f;
if (!fOpt.isPresent()) {
f = new Foo(t.getCategory(), 0, 0);
summarized.add(f);
} else {
f = fOpt.get();
}
f.setAmount(f.getAmount() + t.getAmount());
f.setPrice(f.getPrice() + t.getPrice());
return f;
}
}));
System.out.println(collect);
}