web-dev-qa-db-fra.com

Grouper et sommer des objets comme en SQL avec des lambdas Java?

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?

35
haisi

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

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

4
user1241671

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]]
1
Lukas Eder
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));
}
0
Shylock.Gou