J'ai une classe Foo
avec ces champs:
id: int/name; String/targetCost: BigDecimal/actualCost: BigDecimal
Je reçois un arraylist des objets de cette classe. par exemple.:
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)
Je souhaite transformer ces valeurs en créant une somme de "targetCost" et "actualCost" et en regroupant la "ligne", par exemple.
new Foo(1, "P1", 660, 440),
new Foo(2, "P2", 600, 400),
new Foo(3, "P3", 100, 40),
new Foo(4, "P4", 820, 1100)
Ce que j'ai écrit maintenant:
data.stream()
.???
.collect(Collectors.groupingBy(PlannedProjectPOJO::getId));
Comment puis je faire ça?
Utiliser Collectors.groupingBy
est la bonne approche mais au lieu d'utiliser la version à un seul argument qui créera une liste de tous les éléments de chaque groupe, vous devriez utiliser la version à deux arguments qui prend une autre Collector
qui détermine comment agréger les éléments de chaque groupe .
Cela est particulièrement simple lorsque vous souhaitez agréger une propriété unique des éléments ou simplement compter le nombre d'éléments par groupe:
Compte:
list.stream()
.collect(Collectors.groupingBy(foo -> foo.id, Collectors.counting()))
.forEach((id,count)->System.out.println(id+"\t"+count));
En résumé une propriété:
list.stream()
.collect(Collectors.groupingBy(foo -> foo.id,
Collectors.summingInt(foo->foo.targetCost)))
.forEach((id,sumTargetCost)->System.out.println(id+"\t"+sumTargetCost));
Dans votre cas, lorsque vous souhaitez agréger plusieurs propriétés en spécifiant une opération de réduction personnalisée comme suggéré dans cette réponse est la bonne approche, vous pouvez toutefois appliquer le droit de réduction pendant l'opération de regroupement, de sorte qu'il n'est pas nécessaire de collecter l'intégralité des données dans un Map<…,List>
avant d'effectuer la réduction:
(Je suppose que vous utilisez un import static Java.util.stream.Collectors.*;
maintenant…)
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));
Par souci d'exhaustivité, voici une solution à un problème qui dépasse le cadre de votre question: que se passe-t-il si vous souhaitez GROUP BY
plusieurs colonnes/propriétés?
La première chose qui saute aux yeux des programmeurs est d’utiliser groupingBy
pour extraire les propriétés des éléments du flux et créer/retourner un nouvel objet clé. Mais cela nécessite une classe de détenteur appropriée pour les propriétés de clé (et Java n'a pas de classe Tuple à usage général).
Mais il y a une alternative. En utilisant la forme à trois arguments de groupingBy
, nous pouvons spécifier un fournisseur pour l'implémentation actuelle de Map
qui déterminera l'égalité des clés. En utilisant une carte triée avec un comparateur comparant plusieurs propriétés, nous obtenons le comportement souhaité sans avoir besoin d'une classe supplémentaire. Nous devons seulement veiller à ne pas utiliser les propriétés des instances clés ignorées par notre comparateur, car elles n'auront que des valeurs arbitraires:
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));
Voici une approche possible:
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);
}
}
Sortie:
[Foo(1,P1,660,440), Foo(2,P2,600,400), Foo(3,P3,100,40), Foo(4,P4,820,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();
utilisez simplement toMap (), très simple
Faire cela avec l'API Stream
du JDK n'est pas vraiment simple comme le montrent d'autres réponses. Cet article explique comment obtenir la sémantique SQL de GROUP BY
en Java 8 (avec des fonctions d'agrégation standard) et en utilisant jOOλ , une bibliothèque qui étend Stream
pour ces cas d'utilisation.
Écrire:
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());
Appel
System.out.println(list);
Cédera alors
[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]]
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));
}