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
我正在尝试利用 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