[英]Java Stream GroupBy and Reduce
我有一个项目 class,其中包含一个代码、数量和金额字段,以及一个可能包含许多项目(具有相同代码)的项目列表。 我想按代码对项目进行分组并总结它们的数量和金额。
我能够使用流的groupingBy
和reduce
实现一半。 分组方式有效,但减少是将所有分组项目减少为一个在不同代码上重复的单个项目( groupingBy
键)。
此处不应该减少从 map 中减少每个代码的项目列表吗? 为什么要为所有人重新调整相同的组合项目。
下面是一个示例代码。
import java.util.List;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.Map;
class HelloWorld {
public static void main(String[] args) {
List<Item> itemList = Arrays.asList(
createItem("CODE1", 1, 12),
createItem("CODE2", 4, 22),
createItem("CODE3", 5, 50),
createItem("CODE4", 2, 11),
createItem("CODE4", 8, 20),
createItem("CODE2", 1, 42)
);
Map<String, Item> aggregatedItems = itemList
.stream()
.collect(Collectors.groupingBy(
Item::getCode,
Collectors.reducing(new Item(), (aggregatedItem, item) -> {
int aggregatedQuantity = aggregatedItem.getQuantity();
double aggregatedAmount = aggregatedItem.getAmount();
aggregatedItem.setQuantity(aggregatedQuantity + item.getQuantity());
aggregatedItem.setAmount(aggregatedAmount + item.getAmount());
return aggregatedItem;
})
));
System.out.println("Map total size: " + aggregatedItems.size()); // expected 4
System.out.println();
aggregatedItems.forEach((key, value) -> {
System.out.println("key: " + key);
System.out.println("value - quantity: " + value.getQuantity() + " - amount: " + value.getAmount());
System.out.println();
});
}
private static Item createItem(String code, int quantity, double amount) {
Item item = new Item();
item.setCode(code);
item.setQuantity(quantity);
item.setAmount(amount);
return item;
}
}
class Item {
private String code;
private int quantity;
private double amount;
public Item() {
quantity = 0;
amount = 0.0;
}
public String getCode() { return code; }
public int getQuantity() { return quantity; }
public double getAmount() { return amount; }
public void setCode(String code) { this.code = code; }
public void setQuantity(int quantity) { this.quantity = quantity; }
public void setAmount(double amount) { this.amount = amount; }
}
以下是 output。
Map total size: 4
key: CODE2
value - quantity: 21 - amount: 157.0
key: CODE1
value - quantity: 21 - amount: 157.0
key: CODE4
value - quantity: 21 - amount: 157.0
key: CODE3
value - quantity: 21 - amount: 157.0
您不得将输入 arguments 修改为Collectors.reducing
。 new Item()
只执行一次,所有归约操作将共享相同的“聚合实例”。 换句话说:map 将包含相同的值实例 4 次(您可以使用System.identityHashCode()
或通过比较参考相等性轻松检查自己: aggregatedItems.get("CODE1") == aggregatedItems.get("CODE2")
)。
相反,返回一个新的结果实例:
final Map<String, Item> aggregatedItems = itemList
.stream()
.collect(Collectors.groupingBy(
Item::getCode,
Collectors.reducing(new Item(), (item1, item2) -> {
final Item reduced = new Item();
reduced.setQuantity(item1.getQuantity() + item2.getQuantity());
reduced.setAmount(item1.getAmount() + item2.getAmount());
return reduced;
})
));
Output:
Map total size: 4
key: CODE2
value - quantity: 5 - amount: 64.0
key: CODE1
value - quantity: 1 - amount: 12.0
key: CODE4
value - quantity: 10 - amount: 31.0
key: CODE3
value - quantity: 5 - amount: 50.0
您正在使用reducing
,它假定您不会改变传入的累加器。 reducing
不会为每个新组创建新Item
,并希望您创建新Item
并将它们返回 lambda 中,就像这样:
// this works as expected
.collect(Collectors.groupingBy(
Item::getCode,
Collectors.reducing(new Item(), (item1, item2) -> createItem(
item1.getCode(),
item1.getQuantity() + item2.getQuantity(),
item1.getAmount() + item2.getAmount()
))
));
因此,如果您使用数字或字符串等不可变对象,它非常适合。
由于您没有在代码中创建新的Item
,因此 reduce 会继续重用同一实例, reducing
导致您看到的行为。
如果要改变对象,可以使用Collector.of
以线程安全的方式进行可变归约:
.collect(Collectors.groupingBy(
Item::getCode,
Collector.of(Item::new, (aggregatedItem, item) -> {
int aggregatedQuantity = aggregatedItem.getQuantity();
double aggregatedAmount = aggregatedItem.getAmount();
aggregatedItem.setQuantity(aggregatedQuantity + item.getQuantity());
aggregatedItem.setAmount(aggregatedAmount + item.getAmount());
}, (item1, item2) -> createItem(
item1.getCode(),
item1.getQuantity() + item2.getQuantity(),
item1.getAmount() + item2.getAmount()
))
));
请注意,您现在将引用传递给Item
的构造函数,即一种在必要时创建新Item
的方法,而不仅仅是一个new Item()
。 此外,您还提供了第三个参数,combiner,它告诉收集器如何从两个现有项目中创建一个新项目,如果此收集器在并发情况下使用,则将使用该参数。 (有关组合器的更多信息,请参见此处)
Collector.of
和Collectors.reducing
之间的这种对比与Stream.reduce
和Stream.collect
之间的对比相同。 在这里了解更多。
在这种情况下, Collectors.reducing()
不是正确的工具,因为它意味着不可变归约,即执行折叠操作,其中每个归约步骤都会导致创建新的不可变 object。
但是,不是在每个缩减步骤中生成新的 object,而是更改作为身份提供的 object 的 state 。
因此,您得到的结果不正确,因为每个线程只会创建一次身份object。 这个Item
的单个实例用于累积,并在 map 的每个值中引用它。
您可以在Stream API 文档中找到更详细的信息,特别是在以下部分: Reduction和Mutable Reduction 。
这是一个简短的引述,解释了Stream.reduce()
的工作原理( Collectors.reducing()
背后的机制是相同的):
累加器 function获取部分结果和下一个元素,并产生新的部分结果。
这个问题可以通过生成一个新的Item
实例来解决,同时累积映射到同一个键的值,但更高效的方法是使用可变归约。
为此,您可以实现通过 static 方法Collector.of()
创建的自定义收集器:
Map<String, Item> aggregatedItems = itemList.stream()
.collect(Collectors.groupingBy(
Item::getCode,
Collector.of(
Item::new, // mutable container of the collector
Item::merge, // accumulator - defines how stream data should be accumulated
Item::merge // combiner - mergin the two containers while executing stream in parallel
)
));
为方便起见,您可以引入方法merge()
负责累积两个项目的属性。 它将允许避免在accumulator和combiner中重复相同的逻辑,并保持收集器实现的精简和可读性。
public class Item {
private String code;
private int quantity;
private double amount;
// getters, constructor, etc.
public Item merge(Item other) {
this.quantity += other.quantity;
this.amount += other.amount;
return this;
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.