【问题标题】:Java 8 streams group by 3 fields and aggregate by sum and count produce single line outputJava 8 流按 3 个字段分组并按总和和计数聚合产生单行输出
【发布时间】:2017-10-20 17:08:12
【问题描述】:

我知道论坛中有人问过类似的问题,但似乎都没有完全解决我的问题。现在我对 Java 8 很陌生,所以请多多包涵。 我有一个产品列表,例如:

Input:
name    category    type    cost
prod1       cat2     t1      100.23
prod2       cat1     t2      50.23
prod1       cat1     t3      200.23
prod3       cat2     t1      150.23
prod1       cat2     t1      100.23


Output:
Single line (name, category, type) summing the cost and count of products.




Product {
    public String name;
    public String category;
    public String type;
    public int id;
    public double cost;

}

我需要按名称、类别和类型对其进行分组,并生成一个结果 汇总这些数据并生成每个产品的总成本和数量。大多数示例显示按两个字段分组并使用单个条件进行聚合。

根据论坛上的建议,我想出了这个分组:

    public class ObjectKeys {

    ArrayList<Object> keys;

    public ObjectKeys(Object...searchKeys) {

         keys = new ArrayList<Object>();

            for (int i = 0; i < searchKeys.length; i++) {
                keys.add( searchKeys[i] );
            }
    }

}

然后使用如下:

Map<String, Map<String, Map<String, List<Product>>>> productsByNameCategoryType =
    products.stream().collect(groupingBy(new ObjectKeys(l.name(), l.category(),l.type())))

但是我如何将上面的代码链接起来计数和求和?特别是对于超过 2 个字段的分组。 有一个更好的方法吗?

就像我提到的我的 Java8 不是那么好,请帮助。

【问题讨论】:

  • 也许你需要一个Item 类型来保存Productcost。然后你就可以轻松完成你的工作了。
  • @holi-java 你介意为我扩展一下你的想法
  • 对不起,我正在接电话。如果您引入Item 类并从Priduct 中删除cost,您可以按产品对项目进行汇总。

标签: lambda java-8 java-stream chaining collectors


【解决方案1】:

前提条件

class Product {
    public String name;
    public String category;
    public String type;
    public int id; 
    //todo:implement equals(), toString() and hashCode()
 }

class Item{
   public Product product;
   public double cost;
}

总结方式

您可以使用 Collectors#groupingByCollectors#summarizingDouble 来汇总按产品分组的项目。

List<Item> items = ...; 
Map<Product, DoubleSummaryStatistics> stat = items.stream().collect(groupingBy(
            it -> it.product,
            Collectors.summarizingDouble(it -> it.cost)
));

// get some product summarizing
long count = stat.get(product).getCount();
double sum = stat.get(product).getSum();

//list all product summarizing
stat.entrySet().forEach(it ->
  System.out.println(String.format("%s - count: %d, total cost: %.2f"
        , it.getKey(),it.getValue().getCount(), it.getValue().getSum()));
);

合并具有相同产品的项目

首先,您需要在Item 类中添加一个qty 字段:

class Item{
   public int qty;
   //other fields will be omitted

   public Item add(Item that) {
        if (!Objects.equals(this.product, that.product)) {
            throw new IllegalArgumentException("Can't be added items"
                     +" with diff products!");
        }
        return from(product, this.cost + that.cost, this.qty + that.qty);
    }

    private static Item from(Product product, double cost, int qty) {
        Item it = new Item();
        it.product = product;
        it.cost = cost;
        it.qty = qty;
        return it;
    }

}

那么您可以使用Collectors#toMap 来合并具有相同产品的商品:

Collection<Item> summarized = items.stream().collect(Collectors.toMap(
        it -> it.product,
        Function.identity(),
        Item::add
)).values();

终于

您可以看到两种方法都在做同样的事情,但第二种方法更容易在流上操作。以及我在github上查过的两种方式的测试,你可以点击查看更多详情:summarizing items & merge itemsways。

【讨论】:

  • 感谢holi-java 刚才测试一下
  • 感谢您的帮助,但在此处分组为您的代码时有一个问题:(Collectors.groupingBy(it -> it.product,而不是按一种产品分组,我不应该按名称分组,类型和类别?
  • @Vuzi 嗨,我已经在 cmets 中提到了两种方法都可以,但是将数据打包到对象中会让你的工作更轻松愉快。并且您在评论中建议您需要一个ProductSummary 您可以在Item 中添加一个qty 并仅按产品将项目分组到项目中。另一方面,您希望结果类型是List&lt;Item&gt; 而不是Map&lt;Product, DoubleSummaryStatistics&gt; .
  • @Vuzi 对不起,我昨晚在手机上回答了你的问题,很不方便,我现在为你的问题提供两种方法。您可以尝试并思考:“我为什么建议您将字段打包到对象中?”。
【解决方案2】:

这是快速而肮脏的解决方案:

    Map<String, String> productsByNameCategoryType = products.stream()
            .collect(Collectors.groupingBy(p 
                            -> p.getName() + '-' + p.getCategory() + '-' + p.getType(),
                    Collectors.collectingAndThen(
                            Collectors.summarizingDouble(Product::getCost),
                            dss -> String.format("%7.2f%3d", 
                                                 dss.getSum(), dss.getCount()))));

您可能希望为结果映射的键和值构建自己的类。无论如何,使用您的数据和上述代码,地图包含四个条目:

prod1-cat1-t3:  200,23  1
prod1-cat2-t1:  200,46  2
prod3-cat2-t1:  150,23  1
prod2-cat1-t2:   50,23  1

总和以逗号作为小数点打印,因为我的计算机具有丹麦语言环境(如果需要,您可以将语言环境传递给 String.format() 以控制语言环境)。

您的朋友是Collectors.collectingAndThen()Collectors.summarizingDouble() 的组合。我是从this answer 那里得到的。

【讨论】:

  • 这也可以,特别感谢 Collectors.collectingAndThen() 和 Collectors.summarizingDouble(),它们确实非常方便。虽然方法很“脏”,但它令人大开眼界
猜你喜欢
  • 2021-04-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-14
  • 2021-09-25
  • 2018-07-24
  • 2017-04-26
  • 1970-01-01
相关资源
最近更新 更多