像Java一样用Java lambdas分组和汇总对象?

时间:2014-10-13 13:04:39

标签: java lambda java-8 java-stream

我有一个课程Foo,其中包含以下字段:

  

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

我得到了这个类的对象的arraylist。例如:

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"并将" row"分组e.g。

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 个答案:

答案 0 :(得分:57)

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

答案 1 :(得分:14)

这是一种可能的方法:

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

答案 2 :(得分:4)

removeAttr

只需使用toMap(),非常简单

答案 3 :(得分:2)

仅使用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]]

答案 4 :(得分:0)

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