分组对象并求和对象,就像在 SQL 中使用 Java lambdas 一样?

2022-08-31 17:25:44

我有一个包含这些字段的类:Foo

id:int / name;字符串 / 目标成本:大十进制 / 实际成本:大十进制

我得到了这个类对象的数组列表,例如:

new Foo(1, "P1", 300, 400), 
new Foo(2, "P2", 600, 400),
new Foo(3, "P3", 30, 20),
new Foo(3, "P3", 70, 20),
new Foo(1, "P1", 360, 40),
new Foo(4, "P4", 320, 200),
new Foo(4, "P4", 500, 900)

我想通过创建“targetCost”和“actualCost”的总和并对“行”进行分组来转换这些值,例如

new Foo(1, "P1", 660, 440),
new Foo(2, "P2", 600, 400),
new Foo(3, "P3", 100, 40),
new Foo(4, "P4", 820, 1100)

我现在写的东西:

data.stream()
       .???
       .collect(Collectors.groupingBy(PlannedProjectPOJO::getId));

我该怎么做?


答案 1

使用是正确的方法,但不是使用单个参数版本,这将为每个组创建所有项目的列表,您应该使用两个arg版本,该版本采用另一个确定如何聚合每个组的元素。Collectors.groupingByCollector

当您想要聚合元素的单个属性或仅计算每个组的元素数时,这一点尤其流畅:

  • 计数:

    list.stream()
      .collect(Collectors.groupingBy(foo -> foo.id, Collectors.counting()))
      .forEach((id,count)->System.out.println(id+"\t"+count));
    
  • 总结一个属性:

    list.stream()
      .collect(Collectors.groupingBy(foo -> foo.id,
                                        Collectors.summingInt(foo->foo.targetCost)))
      .forEach((id,sumTargetCost)->System.out.println(id+"\t"+sumTargetCost));
    

但是,如果您想要聚合多个属性,指定自定义约简操作(如本答案中建议的那样)是正确的方法,但是,您可以在分组操作期间执行右约化,因此无需在执行归约之前将整个数据收集到 中:Map<…,List>

(我假设你使用现在...)import static java.util.stream.Collectors.*;

list.stream().collect(groupingBy(foo -> foo.id, collectingAndThen(reducing(
  (a,b)-> new Foo(a.id, a.ref, a.targetCost+b.targetCost, a.actualCost+b.actualCost)),
      Optional::get)))
  .forEach((id,foo)->System.out.println(foo));

为了完整性,这里有一个超出您问题范围的问题的解决方案:如果要使用多个列/属性,该怎么办?GROUP BY

进入程序员脑海的第一件事是使用来提取流元素的属性并创建/返回一个新的键对象。但这需要一个合适的持有者类作为关键属性(Java没有通用的元组类)。groupingBy

但还有另一种选择。通过使用分组的三参数形式我们可以为实际实现指定一个供应商,这将确定关键相等性。通过将排序映射与比较器一起比较多个属性,我们获得了所需的行为,而无需额外的类。我们只需要注意不要使用比较器忽略的关键实例中的属性,因为它们将只有任意值:Map

list.stream().collect(groupingBy(Function.identity(),
  ()->new TreeMap<>(
    // we are effectively grouping by [id, actualCost]
    Comparator.<Foo,Integer>comparing(foo->foo.id).thenComparing(foo->foo.actualCost)
  ), // and aggregating/ summing targetCost
  Collectors.summingInt(foo->foo.targetCost)))
.forEach((group,targetCostSum) ->
    // take the id and actualCost from the group and actualCost from aggregation
    System.out.println(group.id+"\t"+group.actualCost+"\t"+targetCostSum));

答案 2

以下是一种可能的方法:

public class Test {
    private static class Foo {
        public int id, targetCost, actualCost;
        public String ref;

        public Foo(int id, String ref, int targetCost, int actualCost) {
            this.id = id;
            this.targetCost = targetCost;
            this.actualCost = actualCost;
            this.ref = ref;
        }

        @Override
        public String toString() {
            return String.format("Foo(%d,%s,%d,%d)",id,ref,targetCost,actualCost);
        }
    }

    public static void main(String[] args) {
        List<Foo> list = Arrays.asList(
            new Foo(1, "P1", 300, 400), 
            new Foo(2, "P2", 600, 400),
            new Foo(3, "P3", 30, 20),
            new Foo(3, "P3", 70, 20),
            new Foo(1, "P1", 360, 40),
            new Foo(4, "P4", 320, 200),
            new Foo(4, "P4", 500, 900));

        List<Foo> transform = list.stream()
            .collect(Collectors.groupingBy(foo -> foo.id))
            .entrySet().stream()
            .map(e -> e.getValue().stream()
                .reduce((f1,f2) -> new Foo(f1.id,f1.ref,f1.targetCost + f2.targetCost,f1.actualCost + f2.actualCost)))
                .map(f -> f.get())
                .collect(Collectors.toList());
        System.out.println(transform);
    }
}

输出:

[Foo(1,P1,660,440), Foo(2,P2,600,400), Foo(3,P3,100,40), Foo(4,P4,820,1100)]

推荐