在Java的lambda表达式中,类似于SQL那样按组分组和求和对象?

62

我有一个名为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)

我想通过创建“目标成本”和“实际成本”的总和并对“行”进行分组来转换这些值。


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

我该怎么做?

5个回答

121
使用Collectors.groupingBy是正确的方法,但不要使用单参数版本,因为它会为每个组创建一个所有项目的列表,而应该使用两个参数的版本,它需要另一个Collector来确定如何聚合每个组的元素。
当您想要聚合元素的单个属性或仅计算每个组的元素数量时,这尤其顺畅:
  • 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));
    
在您的情况下,当您想要聚合多个属性并指定自定义缩减操作时,像此答案中建议的那样是正确的方法,但是您可以在分组操作期间执行缩减,因此在执行缩减之前无需将整个数据收集到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类)。但有一种另外的方法。通过使用三参数形式的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
很好,我实际上从未使用过Collectors的这些方法。那应该是被接受的答案。 - Dici
@Holger,请问如何在Java 7中实现? - hamza-don
2
@don-kaotic:那是完全不同的问题。 - Holger
1
@hamza-don 我相信你现在已经知道在Java 7中是不可能的了。 - Sayantan
在我的情况下,我有一个名为XYZ的类,它具有类型为“Foo”的元素列表。我想按照上面的逻辑进行groupBy,然后需要用Foo列表替换它。因此,我必须像这样执行操作:xyz.setFooList(performAboveOperation(xyz.getFooList()))。我必须在xyz中使用缩小的foo列表替换foo列表。有什么建议吗?如何在列表中收集结果而不是在该流上调用forEach? - doga
1
@doga 我认为你应该提出一个新问题,包括你已经尝试过的内容和这个问答的链接(如果你愿意),以提供更多的背景信息。 - Holger

24

以下是一种可能的方法:

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) -> { f1.targetCost += f2.targetCost; f1.actualCost += f2.actualCost; return f1;})可行吗? - Sobvan
1
使用函数式风格的一般规则是函数应该是纯的,这意味着没有任何副作用。每次创建新引用都有一定的成本,但对于绝大多数应用程序来说,这个成本应该是可以忽略的。如果您真的关心性能,请不要使用流,因为与简单循环相比,它们会引入开销。 - Dici
谢谢@Dici。在深入了解这个话题后,我发现stream().collect()而不是stream().reduce()正是我想要的,因为我不想在每次迭代时生成一个新对象。这篇文章对于理解collect()非常有用:https://www.javabrahman.com/java-8/java-8-java-util-stream-collector-basics-tutorial-with-examples/。 - Sobvan

9
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(),非常简单


7
使用JDK的Stream API进行此操作并不像其他答案所示那样简单。 本文解释了如何在Java 8中实现GROUP BY的SQL语义(使用标准聚合函数),并使用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]]

1
只是一个提示,如果您已经有了一个列表,那么您可以传递 Seq.of(yourList.toArray()).ofType(YourListType.class) ... - Rodolfo Faquin
@RodolfoFaquin:你为什么要这样做? - Lukas Eder
例如,如果您有一个由请求填充的 List<YourListType> 并且需要对其进行分组,那么您可以像我的示例一样操作。您还有其他建议吗? - Rodolfo Faquin
@RodolfoFaquin 只需使用 Seq.seq(list) - Lukas Eder

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接