首页 > 解决方案 > Java Collectors.Stream:POJO 构建和设置多个聚合值

问题描述

我正在尝试利用 Collectors.Stream() 库来进行各种数据聚合和操作。现在我的数据集可以在几千条记录到几百万条记录之间。

假设我们有以下 POJO 类:

public class Item{
   String name;
   Double quantity;
   Double price;
   Double totalDollarAmount;

    public Item(String name, Double quantity, Double price) {
        this.name = name;
        this.quantity= quantity;
        this.price = price;
    }

   //Basic Getters and setters

   public Double getTotalDollarAmount(){
      return getQuantity()*getPrice();
   }
}

List<Item>我希望能够快速计算出我购买了每件商品的数量、平均价格以及为该商品花费的总金额。假设对于这种情况,我有以下列表:

        List<Item> itemsOnly = Arrays.asList(
                new Item("apple", 10.0, 9.99),
                new Item("banana", 20.0, 19.99),
                new Item("orange", 10.0, 29.99),
                new Item("watermelon", 10.0, 29.99),
                new Item("papaya", 20.0, 9.99),
                new Item("apple", 100.0, 9.99),
                new Item("apple", 20.0, 9.99)
        );

如果我想获取该列表中每个唯一项目的总数量、平均价格和总美元金额,我可以这样做:

System.out.println("Total Quantity for each Item: " + itemsOnly.stream().collect(
                Collectors.groupingBy(Item::getName, Collectors.summingDouble(Item::getQuantity))));
System.out.println("Average Price for each Item: " + itemsOnly.stream().collect(
                Collectors.groupingBy(Item::getName, Collectors.averagingDouble(Item::getPrice))));
System.out.println("Total Dollar Amount for each Item: " + itemsOnly.stream().collect(
                Collectors.groupingBy(Item::getName, Collectors.summingDouble(Item::getTotalDollarAmount))));

这将返回以下内容:

Total Quantity for each Item: {papaya=20.0, orange=10.0, banana=20.0, apple=130.0, watermelon=10.0}
Average Price for each Item: {papaya=9.99, orange=29.99, banana=19.99, apple=9.99, watermelon=29.99}
Total Dollar Amount for each Item: {papaya=199.8, orange=299.9, banana=399.79999999999995, apple=1298.7, watermelon=299.9}

现在,我想做的是将这些值中的每一个存储到一个新Item对象中。

在上面的示例中,我将有一个名称设置为“apple”的新对象,数量 = 130.0,价格 = 9.99,总金额 = 1298.7。

我希望能够创建这个新Item的,而无需循环遍历我想要的项目名称列表并在三个不同的地图(数量、平均价格、总量)上调用 getter。我不确定这是否可行,但理想情况下,我可以得到一个映射,其中键是项目的名称,值是完全定义的类Item,例如Map<String,Item>.

有没有办法使用收集器流来做到这一点?有没有更好的方法在 Java 中对大型数据集进行快速聚合?

标签: javajava-streamdata-analysiscollectors

解决方案


您快到了。要将分组的项目合并为一个,您可以使用减少收集器

这是一种方法:

首先,定义一种合并两个 Item 的方法:

public static Item merge (Item i1, Item i2) {
    final double count = i1.quantity + i2.quantity;
    final double avgPrice = (i1.quantity * i1.price + i2.quantity * i2.price) / count;
    return new Item(i1.name, count, avgPrice);
}

然后,将其用于分组操作的下游收集器。这是带有减速器的完整 Main:

import java.util.Map;
import java.util.List;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.Optional;

public class Main
{
    public static void main(String[] args) {        
        List<Item> itemsOnly = Arrays.asList(
                new Item("apple", 10.0, 9.99),
                new Item("banana", 20.0, 19.99),
                new Item("orange", 10.0, 29.99),
                new Item("watermelon", 10.0, 29.99),
                new Item("papaya", 20.0, 9.99),
                new Item("apple", 100.0, 9.99),
                new Item("apple", 20.0, 9.99)
        );
        
        Map<String, Item> groupedItems = itemsOnly.stream().collect(
                Collectors.groupingBy(
                     item -> item.name,
                     Collectors.collectingAndThen(
                         Collectors.<Item>reducing(Main::merge),
                         Optional::get // No need for null check: grouping should send at least one element to the reducer
                    )
                )
        );

        for (Item i : groupedItems.values()) System.out.println(i);                 
    }
    
    public static Item merge (Item i1, Item i2) {
        final double count = i1.quantity + i2.quantity;
        final double avgPrice = (i1.quantity * i1.price + i2.quantity * i2.price) / count;
        return new Item(i1.name, count, avgPrice);
    }
    
    public static class Item {
        public final String name;
        public final double quantity;
        public final double price;

        public Item(String name, double quantity, double price) {
            this.name = name;
            this.quantity= quantity;
            this.price = price;
        }

        public double getTotalDollarAmount(){
          return quantity*price;
        }
        
        public String toString() { return String.format("%s: quantity: %d, price: %f, total: %f", name, (int) quantity, price, getTotalDollarAmount()); }
    }
}
 

编辑

正如@Naman 在评论中所说,groupingBy + reduction 的更简单替代方法是使用 toMap 收集器。流调用将如下所示:

Map<String, Item> groupedItems = itemsOnly.stream().collect(
            Collectors.toMap(
                item -> item.name,
                Function.identity(),
                Main::merge
            )
);

一般来说,我的建议是仔细阅读收集器和其他流操作的官方 apidoc,因为每个都有不同的计算属性(有些可以并行运行,有些则不能,在某些情况下您可能需要提供函数, ETC。)。正如您在我的回答中看到的那样,为用例选择更好的可能会很棘手。


推荐阅读