我有以下格式的数据:
ProductName | Date
------------|------
ABC | 1-May
ABC | 1-May
XYZ | 1-May
ABC | 2-May
它采用List的形式,其中Product包含ProductName和Date。现在我想对这些数据进行分组,并按如下方式计算总和:
1-May
-> ABC : 2
-> XYZ : 1
-> Total : 3
2-May
-> ABC: 1
-> Total : 1
到目前为止,我所取得的成果是计数,但不是总值。
myProductList.stream()
.collect(Collectors.groupingBy(Product::getDate,
Collectors.groupingBy(Product::getProductName, Collectors.counting())));
不确定如何获得总价值。
答案 0 :(得分:3)
您可以使用Collectors.collectingAndThen
将条目添加到每个内部地图:
Map<LocalDate, Map<String, Long>> result = myProductList.stream()
.collect(Collectors.groupingBy(
Product::getDate,
TreeMap::new, // orders entries by key, i.e. by date
Collectors.collectingAndThen(
Collectors.groupingBy(
Product::getProductName,
LinkedHashMap::new, // LinkedHashMap is mutable and
Collectors.counting()), // preserves insertion order, i.e.
map -> { // we can insert the total later
map.put("Total", map.values().stream().mapToLong(c -> c).sum());
return map;
})));
result
地图包含:
{2017-05-01={ABC=2, XYZ=1, Total=3}, 2017-05-02={ABC=1, Total=1}}
我已经为外部地图和内部地图指定了供应商。外部地图是TreeMap
,它按键对其条目进行排序(在本例中为日期)。对于内部地图,我决定选择LinkedHashMap
,它是可变的并保留插入顺序,即一旦内部地图填充了数据,我们将能够稍后插入总数。
到目前为止一切顺利。但是,我认为我们可以做得更好,因为一旦每个内部地图都填充了数据,我们需要遍历其所有值来计算总数。 (这是map.values().stream().mapToLong(c -> c).sum()
实际做的事情)。通过这样做,我们没有利用这样一个事实:当计数时,流的每个元素不仅将1
添加到它所属的组中,而且还添加到总数中。幸运的是,我们可以通过自定义收集器来解决这个问题:
public static <T, K> Collector<T, ?, Map<K, Long>> groupsWithTotal(
Function<? super T, ? extends K> classifier,
K totalKeyName) {
class Acc {
Map<K, Long> map = new LinkedHashMap<>();
long total = 0L;
void accumulate(T elem) {
this.map.merge(classifier.apply(elem), 1L, Long::sum);
this.total++;
}
Acc combine(Acc another) {
another.map.forEach((k, v) -> {
this.map.merge(k, v, Long::sum);
this.total += v;
});
return this;
}
Map<K, Long> finish() {
this.map.put(totalKeyName, total);
return this.map;
}
}
return Collector.of(Acc::new, Acc::accumulate, Acc::combine, Acc::finish);
}
此收集器不仅计算每个组的元素(如Collectors.groupingBy(Product::getProductName, Collectors.counting())
所做的那样),还会在累积和组合时增加总数。完成后,它还会添加一个总数。
要使用它,只需调用groupsWithTotal
辅助方法:
Map<LocalDate, Map<String, Long>> result = myProductList.stream()
.collect(Collectors.groupingBy(
Product::getDate,
TreeMap::new,
groupsWithTotal(Product::getProductName, "Total")));
输出相同:
{2017-05-01={ABC=2, XYZ=1, Total=3}, 2017-05-02={ABC=1, Total=1}}
作为奖励,给定LinkedHashMap
支持null
个密钥,此自定义收集器也可以按null
密钥分组,即在Product
具有null
的极少数情况下productName
null
,它会使用NullPointerException
密钥创建一个条目,而不是抛出\test\d1\d2\d3\000.png
\test\d4\d5\d6\000.png
\test\d7\d8\d9\000.png
。
答案 1 :(得分:1)
此解决方案与此处发布的其他解决方案类似,但存在一些主要差异。我已将内部分组逻辑与其自己的方法分开,以使代码更易于管理。该方法采用通用的Total键,并在与预先存在的键冲突时抛出错误。它还接受地图供应商以确保生成的地图是可变的。
public static <T, K, M extends Map<K, Long>> Collector<T, ?, M> countingGroups(
Function<? super T, ? extends K> classifier, Supplier<? extends M> mapFactory, K totalKey) {
return Collectors.collectingAndThen(
Collectors.groupingBy(classifier, mapFactory, Collectors.counting()),
m -> {
long totalValue = m.values().stream().mapToLong(Long::longValue).sum();
if (m.put(totalKey, totalValue) != null) {
throw new IllegalStateException("duplicate mapping found for total key");
}
return m;
});
}
我们现在可以将其用作日期分组的下游收集器,类似于您的初始尝试:
Map<Date, Map<String, Long>> counts = myProductList.stream()
.collect(Collectors.groupingBy(Product::getDate,
countingGroups(Product::getProductName, HashMap::new, "Total")));
答案 2 :(得分:0)
因此,在计算分组之后,您已经达到了类似的目标:
Map<String, Map<String, Long>> products
现在需要一个从键(日期)到嵌套映射中相应值总和的映射。以下是实现这一目标的方法:
Map<String, Long> totals = products.entrySet().stream()
.collect(toMap(Entry::getKey,
e -> e.getValue().values().stream().mapToLong(i -> i).sum()));
答案 3 :(得分:0)
我创建了一个小程序来获取您正在寻找的结果。
此外,您应该看到Collectors.collectingAndThen
。
CollectingAndThen是一个特殊的收集器,允许在收集结束后直接对结果执行另一个操作。
我使用lombok
来避免浪费时间编写构造函数和getter/setters
。
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Getter @Setter
@AllArgsConstructor
class Product {
private String name;
private Long date;
}
public class Practice1 {
public static void main(String... args) {
final List<Product> list = new ArrayList<>();
list.add(new Product("ABC", 1L));
list.add(new Product("ABC", 1L));
list.add(new Product("XYZ", 1L));
list.add(new Product("ABC", 2L));
Map<Long, Map<String, Long>> finalMap = list.stream()
.collect(
Collectors.groupingBy(
Product::getDate,
Collectors.collectingAndThen(
Collectors.groupingBy(Product::getName, Collectors.counting()),
map -> {
long sum = map.values().stream().reduce(0L, Long::sum);
map.put("total", sum);
return map;
}
)
)
);
System.out.println(finalMap);
}
}