Java流分组依据并求和多个字段

时间:2018-08-28 12:44:35

标签: java java-8 java-stream grouping

我有一个列表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键即可。这就是我卡住的地方。我似乎找不到任何方法。

5 个答案:

答案 0 :(得分:15)

有点丑陋,但是应该可以工作:

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

答案 1 :(得分:4)

我的Sweepers answer的变体使用精简的收集器,而不是两次进行流传输以对各个字段求和:

        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实现,这些实现只考虑category才能使生成的map正常工作。一般来说,这可能不是您想要Foo所需要的。我希望定义一个单独的FooSummary类来包含聚合数据。

答案 2 :(得分:3)

如果您有一个特殊的专用构造函数,并且在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的实现,如果它们属于相同的Foo,则认为这两个不同的category实例是相等的。也许这是不希望的,或者您已经具有将更多或其他属性考虑在内的实现。

另一方面,使用Foo作为映射的键(用于按实例的属性之一对实例进行分组)是非常不常见的用例。我认为最好使用category属性按类别分组并具有两个映射:Map<String, List<Foo>>保持分组,Map<String, Foo>保持聚合的priceamount,两种情况下的密钥均为category

此外,此解决方案在将条目放入地图后对地图的键进行突变。这很危险,因为这可能会破坏地图。但是,在这里,我仅对FoohashCodeequals Foo既不参与的属性进行突变。我认为,由于要求的特殊性,这种风险在这种情况下是可以接受的。

答案 3 :(得分:1)

我建议您创建一个帮助类,其中包含金额和价格

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

答案 4 :(得分:0)

我的解决方案:)

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