【问题标题】:Java stream group by and sum multiple fieldsJava流分组并汇总多个字段
【发布时间】:2019-02-03 02:38:59
【问题描述】:

我有一个列表 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吗?
  • 是的,但是你如何总结多个字段?
  • 好吧,直到最近我才回答了一些非常接近这个的问题...... jdk-12 中的stackoverflow.com/a/52041895/1059372 似乎会有一个BiCollector 让你的生活更轻松
  • 我对您的理解正确吗, KEY 将保存总和?并且 List 应该包含所有原始实例?
  • @jokster 是的,没错!

标签: java java-8 java-stream grouping


【解决方案1】:

有点难看,但应该可以:

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

【讨论】:

    【解决方案2】:

    我的Sweeper's 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 正常工作。一般来说,这可能不是您想要的Foos。我更愿意定义一个单独的 FooSummary 类来包含聚合数据。

    【讨论】:

    • 解决了我的问题。谢谢!
    【解决方案3】:

    如果您有一个特殊的、专用的构造函数以及hashCodeequals 方法在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);
    }
    

    上面的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&lt;String, List&lt;Foo&gt;&gt; 保留组,Map&lt;String, Foo&gt; 保留聚合的priceamount,关键是在这两种情况下都是category

    除此之外,此解决方案在将条目放入其中后会改变映射的键。这很危险,因为这可能会破坏地图。但是,在这里我只是改变Foo 的属性,它们既不参与hashCode 也不参与equals Foo 的实现。由于需求的不寻常,我认为这种风险在这种情况下是可以接受的。

    【讨论】:

      【解决方案4】:

      我建议你创建一个辅助类,它会保存数量和价格

      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 需要的...阅读问题下的 cmets
      【解决方案5】:

      我对解决方案的看法:)

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

      【讨论】:

        【解决方案6】:

        你可以这样得到总和,

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-03-07
          • 1970-01-01
          • 2018-03-27
          • 2023-01-12
          • 1970-01-01
          相关资源
          最近更新 更多