簡體   English   中英

Java 流分組按多個字段求和

[英]Java stream group by and sum multiple fields

我有一個列表 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()));

現在我只需要用一個包含匯總金額和價格的 Foo 對象替換 String 鍵。 這是我被困的地方。 我似乎找不到任何方法來做到這一點。

有點難看,但它應該工作:

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答案的變體使用了一個簡化的收集器而不是兩次流式傳輸來對各個字段求和:

        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

但請注意, Foo需要提供hashCode - 和equals -implementations,它們只考慮category以使結果map正常工作。 對於Foo這可能不是你想要的。 我寧願定義一個單獨的FooSummary類來包含聚合數據。

如果您有一個特殊的,專用的構造函數和hashCode並且equalsFoo一致實現的方法,如下所示:

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

上面的hashCodeequals實現允許您在地圖中使用Foo作為有意義的鍵(否則您的地圖將被破壞)。

現在,借助Foo中的新方法同時執行amountprice屬性的聚合,您可以分兩步完成所需的操作。 首先是方法:

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

編輯:遺漏了一些觀察結果......

一方面,此解決方案強制您使用hashCodeequals的實現,如果它們屬於同一category則將兩個不同的Foo實例視為相等。 也許這不是所希望的,或者您已經有一個實現需要考慮更多或其他屬性。

另一方面,使用Foo作為映射的鍵,用於通過其屬性之一對實例進行分組是一個非常罕見的用例。 我認為最好只使用category屬性按類別分組並有兩個映射: Map<String, List<Foo>>保持組和Map<String, Foo>保持匯總的priceamount ,關鍵是兩種情況下的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);
}

你可以得到這樣的總和,

ArrayList<Foo> list = new ArrayList<>();
list.add(new Foo("category_1", 10, 20));
list.add(new Foo("category_2", 11, 21));
list.add(new Foo("category_3", 12, 22));
list.add(new Foo("category_1", 13, 23));
list.add(new Foo("category_2", 14, 24));
list.add(new Foo("category_2", 15, 25));

Map<String, Foo> map = list.stream().collect(Collectors.toMap(Foo::getCategory, Function.identity(), (a1, a2) -> {
    a1.joiner(a2);
    return a1;
}));

確保將此方法也添加到 Foo 類中,

public Foo joiner(Foo that) {
    this.price += that.price;
    this.amount += that.amount;
    return this;
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM