使用 Java lambda 对 SQL 中的对象进行分组和求和?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/26340688/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 02:17:05  来源:igfitidea点击:

Group by and sum objects like in SQL with Java lambdas?

javalambdajava-8java-stream

提问by haisi

I have a class Foowith these fields:

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

id:int / name;String / targetCost:BigDecimal / actualCost:BigDecimal

id:int / name;String / targetCost:BigDecimal / actualCost:BigDecimal

I get an arraylist of objects of this class. e.g.:

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

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)

I want to transform these values by creating a sum of "targetCost" and "actualCost" and grouping the "row" e.g.

我想通过创建“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)

What I have written by now:

我现在写的:

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

How can I do that?

我怎样才能做到这一点?

采纳答案by Holger

Using Collectors.groupingByis the right approach but instead of using the single argument version which will create a list of all items for each group you should use the two arg versionwhich takes another Collectorwhich determines how to aggregate the elements of each group.

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

This is especially smooth when you want to aggregate a single property of the elements or just count the number of elements per group:

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

  • Counting:

    list.stream()
      .collect(Collectors.groupingBy(foo -> foo.id, Collectors.counting()))
      .forEach((id,count)->System.out.println(id+"\t"+count));
    
  • Summing up one property:

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

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

In your case when you want to aggregate more than one property specifying a custom reduction operation like suggested in this answeris the right approach, however, you can perform the reduction right during the grouping operation so there is no need to collect the entire data into a Map<…,List>before performing the reduction:

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

(I assume you use a import static java.util.stream.Collectors.*;now…)

(我假设您使用import static java.util.stream.Collectors.*;now...)

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


For completeness, here a solution for a problem beyond the scope of your question: what if you want to GROUP BYmultiple columns/properties?

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

The first thing which jumps into the programmers mind, is to use groupingByto extract the properties of the stream's elements and create/return a new key object. But this requires an appropriate holder class for the key properties (and Java has no general purpose Tuple class).

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

But there is an alternative. By using the three-arg form of groupingBywe can specify a supplier for the actual Mapimplementation which will determine the key equality. By using a sorted map with a comparator comparing multiple properties we get the desired behavior without the need for an additional class. We only have to take care not to use properties from the key instances our comparator ignored, as they will have just arbitrary values:

但还有一个选择。通过使用 的三参数形式,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));

回答by Dici

Here is one possible approach :

这是一种可能的方法:

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

Output :

输出 :

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

回答by Lukas Eder

Doing this with the JDK's StreamAPI only isn't really straightforward as other answers have shown. This article explains how you can achieve the SQL semantics of GROUP BYin Java 8(with standard aggregate functions) and by using jOOλ, a library that extends Streamfor these use-cases.

Stream仅使用 JDK 的API执行此操作并不像其他答案所示那样简单。本文解释了如何实现GROUP BYJava 8 中的 SQL 语义(使用标准聚合函数)以及如何使用jOOλ(一个Stream针对这些用例进行扩展的库)。

Write:

写:

import static org.jooq.lambda.tuple.Tuple.tuple;

import java.util.List;
import java.util.stream.Collectors;

import org.jooq.lambda.Seq;
import org.jooq.lambda.tuple.Tuple;
// ...

List<Foo> list =

// FROM Foo
Seq.of(
    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))

// GROUP BY f1, f2
.groupBy(
    x -> tuple(x.f1, x.f2),

// SELECT SUM(f3), SUM(f4)
    Tuple.collectors(
        Collectors.summingInt(x -> x.f3),
        Collectors.summingInt(x -> x.f4)
    )
)

// Transform the Map<Tuple2<Integer, String>, Tuple2<Integer, Integer>> type to List<Foo>
.entrySet()
.stream()
.map(e -> new Foo(e.getKey().v1, e.getKey().v2, e.getValue().v1, e.getValue().v2))
.collect(Collectors.toList());

Calling

打电话

System.out.println(list);

Will then yield

然后会屈服

[Foo [f1=1, f2=P1, f3=660, f4=440],
 Foo [f1=2, f2=P2, f3=600, f4=400], 
 Foo [f1=3, f2=P3, f3=100, f4=40], 
 Foo [f1=4, f2=P4, f3=820, f4=1100]]

回答by user1241671

data.stream().collect(toMap(foo -> foo.id,
                       Function.identity(),
                       (a, b) -> new Foo(a.getId(),
                               a.getNum() + b.getNum(),
                               a.getXXX(),
                               a.getYYY()))).values();

just use toMap(), very simple

就用toMap(),很简单

回答by Shylock.Gou

public  <T, K> Collector<T, ?, Map<K, Integer>> groupSummingInt(Function<? super T, ? extends K>  identity, ToIntFunction<? super T> val) {
    return Collectors.groupingBy(identity, Collectors.summingInt(val));
}