Java 8 收集,用总和计数

Vim*_*vam 5 java java-8 java-stream

我有以下格式的数据:

ProductName | Date
------------|------
ABC         | 1-May
ABC         | 1-May
XYZ         | 1-May
ABC         | 2-May
Run Code Online (Sandbox Code Playgroud)

它采用 List 的形式,其中 Product 由 ProductName 和 Date 组成。现在我想对这些数据进行分组并得到总和的计数,如下所示:

1-May
  -> ABC : 2
  -> XYZ : 1
  -> Total : 3
2-May
  -> ABC: 1
  -> Total : 1
Run Code Online (Sandbox Code Playgroud)

到目前为止,我所取得的是分组计数,而不是总价值。

myProductList.stream()
  .collect(Collectors.groupingBy(Product::getDate, 
            Collectors.groupingBy(Product::getProductName, Collectors.counting())));
Run Code Online (Sandbox Code Playgroud)

不知道如何获得总价值。

Fed*_*ner 5

您可以使用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;
            })));
Run Code Online (Sandbox Code Playgroud)

result地图包含:

{2017-05-01={ABC=2, XYZ=1, Total=3}, 2017-05-02={ABC=1, Total=1}}
Run Code Online (Sandbox Code Playgroud)

我已经为外部地图和内部地图指定了供应商。外部映射是 a 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);
}
Run Code Online (Sandbox Code Playgroud)

该收集器不仅对每个组的元素进行计数(就像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")));
Run Code Online (Sandbox Code Playgroud)

输出是相同的:

{2017-05-01={ABC=2, XYZ=1, Total=3}, 2017-05-02={ABC=1, Total=1}}
Run Code Online (Sandbox Code Playgroud)

作为奖励,鉴于LinkedHashMap支持null键,此自定义收集器还可以按键进行分组null,即在极少数情况下 aProduct具有null productName,它将使用该null键创建一个条目而不是抛出 a NullPointerException