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

Posted

技术标签:

【中文标题】使用 Java lambda 对 SQL 中的对象进行分组和求和?【英文标题】:Group by and sum objects like in SQL with Java lambdas? 【发布时间】:2014-12-08 01:25:17 【问题描述】:

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

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

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

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】:

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

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

计数:

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

如果您想要聚合多个属性,指定自定义归约操作like suggested in this answer 是正确的方法,但是,您可以在分组操作期间执行归约权限,因此无需收集整个数据在执行缩减之前转换为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 多个列/属性怎么办?

程序员首先想到的是使用groupingBy 来提取流元素的属性并创建/返回一个新的键对象。但这需要一个适当的持有者类来存放关键属性(Java 没有通用的 Tuple 类)。

但是还有一个选择。通过使用three-arg form of 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));

【讨论】:

很好,我实际上从未使用过Collectors 的那些方法。那应该是公认的答案 @Holger 请问如何在 Java 7 中做到这一点? @don-kaotic:这是一个完全不同的问题 @hamza-don 我相信现在你知道这在 Java 7 中是不可能的 @doga 我认为您应该提出一个新问题,包括您尝试过的内容以及此问答的反向链接(如果您愿意),以提供更多背景信息。【参考方案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)]

【讨论】:

如果我理解正确,您需要在每个 reduce 操作上创建一个新的 Foo 对象,否则,减少对并行操作不利。然而,这是一种资源浪费,因为我们可以就地修改 foo 对象。你怎么看? reduce((f1,f2) -&gt; f1.targetCost += f2.targetCost; f1.actualCost += f2.actualCost; return f1;) 可以工作吗? 使用函数式风格的一般规则是函数应该是纯的,这意味着没有任何副作用。每次创建一个新参考的成本很低,对于绝大多数应用程序来说应该可以忽略不计。如果您真的关心性能,请不要使用流,因为与简单循环相比,它们会带来开销。 谢谢@Dici。在阅读了有关此主题的更多信息后,我发现 stream().collect() 而不是 stream().reduce() 是我不想在每次迭代时产生一个新对象。这篇文章对于理解 collect() 非常有用:javabrahman.com/java-8/…【参考方案3】:

仅使用 JDK 的 Stream API 执行此操作并不像其他答案所示的那样简单。 This article explains how you can achieve the SQL semantics of GROUP BY in Java 8(使用标准聚合函数)并使用 jOOλ,这是一个针对这些用例扩展 Stream 的库。

写:

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

打电话

System.out.println(list);

然后会屈服

[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]]

【讨论】:

只是一个提示,如果你已经有一个列表,那么你可以通过Seq.of(yourList.toArray()).ofType(YourListType.class) ... @RodolfoFaquin:你为什么要这么做? 例如,如果您有一个由请求填充的List&lt;YourListType&gt;,并且您需要对其进行分组,那么您可以像我的示例一样对它们进行分组。你有其他建议如何做到这一点? @RodolfoFaquin 就用Seq.seq(list)【参考方案4】:
data.stream().collect(toMap(foo -> foo.id,
                       Function.identity(),
                       (a, b) -> new Foo(a.getId(),
                               a.getNum() + b.getNum(),
                               a.getXXX(),
                               a.getYYY()))).values();

只用toMap(),很简单

【讨论】:

【参考方案5】:
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));

【讨论】:

以上是关于使用 Java lambda 对 SQL 中的对象进行分组和求和?的主要内容,如果未能解决你的问题,请参考以下文章

Java 8 中的 Lambda 表达式

如何在java 8中将字符串排序为带有lambdas的int

java8 新特性入门 stream/lambda

java中class对象详解,在spring中的使用场景

请教如何使用lambda分组后排序

用不用lambda,这是一个问题