java - 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 中对大型数据集进行快速聚合?
解决方案
您快到了。要将分组的项目合并为一个,您可以使用减少收集器。
这是一种方法:
首先,定义一种合并两个 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。)。正如您在我的回答中看到的那样,为用例选择更好的可能会很棘手。
推荐阅读
- python - 创建具有实际值的虚拟列
- angular - Angular :mat-input 输出在输入值更改时触发两次
- r - 对 data.frame 中的后续条目(重复)求和
- r - 查找节点邻居的度值之和
- java - OpenCV人脸识别Java代码终止JVM
- python - 使用 If 条件迭代字典的函数
- python - 测试因浮点限制导致的舍入误差
- php - 从 php 中的其他表在大型 mysql 表中添加新的关系列
- javascript - 使用 Firebase 规则构建数据
- java - Android 相机在 APi 22(Lollipop)上失败,但在所有其他版本上都可以使用