collection - java stream size
Agrupar por y sumar objetos como en SQL con Java lambdas? (4)
Aquí hay un posible enfoque:
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);
}
}
Salida:
[Foo(1,P1,660,440), Foo(2,P2,600,400), Foo(3,P3,100,40), Foo(4,P4,820,1100)]
Tengo una clase Foo
con estos campos:
id: int / name; String / targetCost: BigDecimal / actualCost: BigDecimal
Obtengo una lista de objetos de esta clase. p.ej:
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)
Quiero transformar estos valores creando una suma de "targetCost" y "realCost" y agrupando la "fila", por ej.
new Foo(1, "P1", 660, 440),
new Foo(2, "P2", 600, 400),
new Foo(3, "P3", 100, 40),
new Foo(4, "P4", 820, 1100)
Lo que he escrito hasta ahora
data.stream()
.???
.collect(Collectors.groupingBy(PlannedProjectPOJO::getId));
¿Cómo puedo hacer eso?
El uso de Collectors.groupingBy
es el enfoque correcto, pero en lugar de utilizar la versión de argumento único que creará una lista de todos los elementos para cada grupo, debe usar la versión de dos arg que toma otro Collector
que determina cómo agregar los elementos de cada grupo.
Esto es especialmente sencillo cuando desea agregar una sola propiedad de los elementos o simplemente contar la cantidad de elementos por grupo:
Contando:
list.stream() .collect(Collectors.groupingBy(foo -> foo.id, Collectors.counting())) .forEach((id,count)->System.out.println(id+"/t"+count));
Resumiendo una propiedad:
list.stream() .collect(Collectors.groupingBy(foo -> foo.id, Collectors.summingInt(foo->foo.targetCost))) .forEach((id,sumTargetCost)->System.out.println(id+"/t"+sumTargetCost));
En su caso, cuando desee agregar más de una propiedad, especificar el modo de reducción personalizado como se sugiere en esta respuesta es el enfoque correcto, sin embargo, puede realizar la reducción directamente durante la operación de agrupación, de modo que no sea necesario recopilar todos los datos en a Map<…,List>
antes de realizar la reducción:
(Supongo que utilizas una 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));
Para completar, aquí una solución para un problema más allá del alcance de su pregunta: ¿y si desea GROUP BY
columnas / propiedades múltiples?
Lo primero que salta a la mente de los programadores, es usar groupingBy
para extraer las propiedades de los elementos de la secuencia y crear / devolver un nuevo objeto clave. Pero esto requiere una clase de titular adecuada para las propiedades clave (y Java no tiene una clase Tuple de propósito general).
Pero hay una alternativa. Al utilizar la forma de groupingBy
de tres arias , podemos especificar un proveedor para la implementación del Map
real que determinará la igualdad de claves. Al usar un mapa ordenado con un comparador que compara propiedades múltiples, obtenemos el comportamiento deseado sin la necesidad de una clase adicional. Solo debemos tener cuidado de no utilizar propiedades de las instancias clave que nuestro comparador ignoró, ya que solo tendrán valores arbitrarios:
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));
Hacer esto solo con la API Stream
de JDK no es muy sencillo ya que se han mostrado otras respuestas. Este artículo explica cómo se puede lograr la semántica de SQL de GROUP BY
en Java 8 (con funciones agregadas estándar) y mediante el uso de jOOλ , una biblioteca que amplía Stream
para estos casos de uso.
Escribir:
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());
Vocación
System.out.println(list);
Entonces rendirá
[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]]
data.stream().collect(toMap(foo -> foo.id,
Function.identity(),
(a, b) -> new Foo(a.getId(),
a.getNum() + b.getNum(),
a.getXXX(),
a.getYYY()))).values();
solo use toMap (), muy simple