Java Collectors.Stream: POJO 构建和设置多个聚合值

Java Collectors.Stream: POJO building and setting multiple aggregated values

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

假设我们有以下 POJO class:

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

这将 return 以下内容:

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 的完全定义的 class,例如Map<String,Item>.

有没有办法使用 Collectors 流来做到这一点? Java 中的大型数据集是否有更好的快速聚合方法?

你快到了。要将分组项目合并为一个项目,您可以使用 reducing collector.

这里有一个方法:

首先,定义合并两个项目的方法:

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 + reducing 的更简单替代方法是使用 toMap 收集器。流调用将如下所示:

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

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

您可以实施 class ItemStats 来收集所有相关统计数据并使用 Collectors.toMap:

进行收集
class ItemStats extends Item {
    private int count;
    
    public ItemStats(Item item) {
        super(item.getName(), item.getQuantity(), item.getPrice());
        this.totalDollarAmount = item.getTotalDollarAmount();
        this.count = 1;
    }
    
    public ItemStats merge(Item item) {
        this.quantity += item.getQuantity();
        this.price += item.getPrice();
        this.totalDollarAmount += item.getTotalDollarAmount();
        this.count++;
        
        return this;
    }
    
    public Double getAveragePrice() {
        return this.price / this.count;
    }
}

// test class
Map<String, ItemStats> stats = itemsOnly
        .stream()
        .collect(Collectors.toMap(
            Item::getName, 
            ItemStats::new, 
            ItemStats::merge, 
            LinkedHashMap::new
        ));
stats.forEach((k, v) -> System.out.printf("%s: total quantity=%.0f avg.price=%.2f total amount=$%.2f%n", 
        k, v.getQuantity(), v.getAveragePrice(), v.getTotalDollarAmount()));

输出:

apple: total quantity=130 avg.price=9.99 total amount=96.10
banana: total quantity=20 avg.price=19.99 total amount=9.80
orange: total quantity=10 avg.price=29.99 total amount=9.90
watermelon: total quantity=10 avg.price=29.99 total amount=9.90
papaya: total quantity=20 avg.price=9.99 total amount=9.80