【问题标题】:How to obtain new list after groupby and sum over one attribute如何在groupby后获取新列表并对一个属性求和
【发布时间】:2017-06-30 06:39:06
【问题描述】:

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

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
}

现在我想将Settlement 的列表按SmartNo(字符串)分组,并得到settleAmount 的总和,这将成为每个SmartNo 的新settleAmount

由于我使用的是 Java 8,stream 应该是要走的路。

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

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

如果我想在按 SmartNo 分组并汇总结算金额后得到一个新列表怎么办?那里的大多数示例仅显示如何打印总和。我感兴趣的是如何获取聚合列表?

【问题讨论】:

  • stream entrySet 和 map,将它发送到您的结果对象,并将 collect 它发送到 List
  • @teppic 你能说得更具体点吗?我对 Java 8 很陌生。不知道如何将 map 它添加到我的结果对象中。
  • 您的结果列表中需要什么类型的对象? SmartNo 和 sum 的简单元组是否足够?
  • @teppic 这两个和另外两个:数量和价格
  • 您想对每个 smartNo 的数量、价格和结算金额求和吗?

标签: java group-by aggregate java-stream collectors


【解决方案1】:

我认为不太复杂的方法是在地图的 values() 的每个成员上创建一个新流,然后是 map()reduce()。我正在映射到一个新类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&lt;AggregatedSettlement&gt; 上的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()而不是flatMap()而不是get()我使用stream(),我将能够避免调用Optional.get() .它将长 6 个字符,我可能仍然更喜欢它。不过,我还没有尝试过 Java 9(现在我知道我今天要做什么 :-) get() 的优势当然是它会捕获内部列表为空的编程错误。

【讨论】:

    【解决方案2】:

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

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

    Settlement 类中使用辅助方法:

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

    【讨论】:

    • 你真的推荐一个可以修改其参数的归约函数吗?
    • @Holger,当然。 Java 不是 Haskell。有时这是很好的解决方案。
    • 我想,你知道这是坏了。您正在修改源列表的任意对象,而无法控制哪些被修改,哪些不被修改。 IE。使用并行流,每次执行时可能会修改不同的对象。
    • @Holger,请给我看一个可能失败的反例,或者给我看一个说明我的解决方案损坏的规范声明。
    • 知道,流处理是如何工作的,例如在合并部分结果之前如何将工作负载拆分为单独处理的块。换句话说,实现可以调用f.apply(a, f.apply(b, c)),而不是f.apply(f.apply(a, b), c)。当然,a 中的结果会得到想要的总和,但b 是否会被修改,是不可预知的。当然,这个问题并没有说明对源对象进行修改的期望行为,但我有点惊讶你推荐这样一个解决方案而没有任何注释
    猜你喜欢
    • 1970-01-01
    • 2020-11-18
    • 1970-01-01
    • 2020-11-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多