如何在 groupby 后获取新列表并对一个属性求和

How to obtain new list after groupby and sum over one attribute

我有一个 Settlement class 的列表,它具有以下属性:

public class Settlement {
    private String contractNo;
    private String smartNo;
    private String dealTrackNo;
    private String buySellFlag;
    private String cashFlowType;
    private String location;
    private String leaseNo;
    private String leaseName;
    private double volume;
    private double price;
    private double settleAmount;

    // getters and setters
}

现在我想按 SmartNo(字符串)对 Settlement 的列表进行分组,并得到 settleAmount 的总和,这成为每个 settleAmount 的新 settleAmount =14=].

因为我使用的是 Java 8,所以 stream 应该是正确的选择。

Groupby 使用以下代码应该非常简单:

Map<String, List<Settlement>> map = list.stream()
              .collect(Collectors.groupingBy(Settlement::getSmartNo));
System.out.println(map.getValues());

如果我想通过SmartNo分组并通过settlementAmount求和得到一个新列表怎么办?那里的大多数例子只展示了如何打印出总和。我感兴趣的是如何获取聚合列表?

我认为 not-too-complex 方式是在地图 values() 的每个成员上创建一个新流,然后是 map()reduce()。我正在映射到一个新的 class AggregatedSettlement,只有三个字段 smartNovolumesettleAmount(最后一个是总和)。然后通过对 settleAmounts.

求和来减少
    List<AggregatedSettlement> aggregatedList = list.stream()
            .collect(Collectors.groupingBy(Settlement::getSmartNo))
            .values()
            .stream()
            .map(innerList -> innerList.stream()
                    .map(settlm -> new AggregatedSettlement(settlm.getSmartNo(), 
                            settlm.getVolume(), settlm.getSettleAmount()))
                    .reduce((as1, as2) -> {
                        if (as1.getVolume() != as2.getVolume()) {
                            throw new IllegalStateException("Different volumes " + as1.getVolume() 
                                    + " and " + as2.getVolume() + " for smartNo " + as1.getSmartNo());
                        }
                        return new AggregatedSettlement(as1.getSmartNo(), as1.getVolume(), 
                                as1.getSettleAmount() + as2.getSettleAmount());
                    })
                    .get()
            )
            .collect(Collectors.toList());

我对 reduce()Optional<AggregatedSettlement> 上打给 get() 的电话不太满意;通常你应该避免 get()。在这种情况下,我知道原始分组只生成至少一个元素的列表,因此 reduce() 不能给出一个空的可选值,因此对 get() 的调用将起作用。可能的改进是 orElseThrow() 和一个更具解释性的异常。

我相信还有优化的空间。我最终生成的 AggregatedSettlement 个对象确实比我们需要的多得多。一如既往,在你知道你需要之前不要优化。

编辑:如果只是为了练习,这里是不构造多余 AggregatedSettlement 对象的版本。相反,它会在地图的每个列表上创建 两个 流,并且长 5 行:

    List<AggregatedSettlement> aggregatedList = list.stream()
            .collect(Collectors.groupingBy(Settlement::getSmartNo))
            .entrySet()
            .stream()
            .map(entry -> {
                double volume = entry.getValue()
                        .stream()
                        .mapToDouble(Settlement::getVolume)
                        .reduce((vol1, vol2) -> {
                            if (vol1 != vol2) {
                                throw new IllegalStateException("Different volumes " + vol1 
                                        + " and " + vol2 + " for smartNo " + entry.getKey());
                            }
                            return vol1;
                        })
                        .getAsDouble();
                double settleAmountSum = entry.getValue()
                        .stream()
                        .mapToDouble(Settlement::getSettleAmount)
                        .sum();
                return new AggregatedSettlement(entry.getKey(), volume, settleAmountSum);
            })
            .collect(Collectors.toList());

选择您认为更容易阅读的那个。

编辑 2:从 this answer 看来,在 Java 9 中,如果我使用 [= 而不是 map(),我将能够避免调用 Optional.get() 31=] 而不是 get() 我使用 stream()。它将长 6 个字符,我可能仍然更喜欢它。不过,我还没有尝试过 Java 9(现在我知道我今天要做什么了:-) get() 的优点当然是它会在内部捕获编程错误列表是空的。

如果我对问题的理解正确,您需要一个 toMap 具有自定义合并功能的收集器,如下所示:

list.stream().collect(Collectors.toMap(
       Settlement::getSmartNo,
       Function.identity(),
       (s1, s2) -> s1.addAmount(s2.getSettleAmount())));

里面有辅助方法 Settlement class:

Settlement addAmount(double addend) {
    this.settleAmount += addend;
    return this;
}