Java流分组并对多个字段求和

21

我有一个列表 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对象替换字符串键即可。这是我卡住的地方。我似乎找不到任何解决方法。


你有查看过Collectors.summarizingLong吗? - dehasi
是的,但是如何对多个字段进行总结? - MatMat
1
最近我回答了一个非常类似的问题... https://stackoverflow.com/a/52041895/1059372 在jdk-12中似乎会有一个BiCollector,这将使您的生活更轻松。 - Eugene
4
我理解你的意思是,KEY应该保存总和?而List<Foo>应该保存所有原始实例? - jokster
@jokster 是的,没错! - MatMat
6个回答

25

有点丑,但应该能用:

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

我尝试使用你的方法,但是无法解决我的问题。这里是链接: https://dev59.com/Dsz6oIgBc1ULPQZF3L1f - qwercan

8
我的变体Sweeper's answer使用一个reducing Collector而不是两次流式处理来计算单个字段的总和:
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需要提供仅考虑categoryhashCodeequals实现。这可能不适用于一般的Foo。我建议定义一个单独的FooSummary类来包含聚合数据。


问题已解决。谢谢! - Walid Ammou

3

如果你在 Foo 类中实现了特殊的、专用的构造函数,以及一致实现的 hashCodeequals 方法,如下所示:

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,在这两种情况下,关键字都是类别。

此外,这个解决方案在把条目放入映射后改变了映射的键。这是危险的,因为这可能会破坏映射。但是,在这里,我只改变了Foo的属性,这些属性既不参与hashCode也不参与equals的实现。我认为,在这种情况下,这种风险是可以接受的,因为该要求不常见。


2
我建议您创建一个辅助类,用于存储数量和价格。
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)));

那是一个不错的开始,但并不是 OP 需要的...请阅读问题下面的评论。 - Eugene

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

0
你可以这样获得总和,
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;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接