Java 8收集,计算总和

时间:2017-05-12 16:42:58

标签: java java-8 java-stream

我有以下格式的数据:

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())));

不确定如何获得总价值。

4 个答案:

答案 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);
    }
}