web-dev-qa-db-ja.com

Javaストリームのグループ化と複数のフィールドの合計

リスト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オブジェクトに置き換えるだけで済みます。私が立ち往生しているところです。私はこれを行う方法を見つけることができないようです。

10
MatMat

少しいですが、動作するはずです:

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));
17
Sweeper

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-の実装を提供するために必要であることに注意してください。正しく動作します。これは、おそらくFoosに必要なものではないでしょう。個別のFooSummaryクラスを定義して、集計データを含めることをお勧めします。

5
Hulk

次のように、特別な専用コンストラクタと、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です。

これに加えて、このソリューションは、エントリが入力された後にマップのキーを変更します。これはマップを壊す可能性があるため、危険です。ただし、ここでは、FoohashCodeequalsの実装にも参加しない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)));
1
dehasi

私の解決策:)

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);
}