it-swarm.com.de

Gruppieren nach und Summe von Objekten wie in SQL mit Java-Lambdas?

Ich habe eine Klasse Foo mit diesen Feldern: 

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

Ich bekomme eine Arrayliste von Objekten dieser Klasse. z.B.: 

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)

Ich möchte diese Werte umwandeln, indem Sie eine Summe aus "targetCost" und "actualCost" erstellen und die "Zeile" z.

new Foo(1, "P1", 660, 440),
new Foo(2, "P2", 600, 400),
new Foo(3, "P3", 100, 40),
new Foo(4, "P4", 820, 1100)

Was ich bis jetzt geschrieben habe:

data.stream()
       .???
       .collect(Collectors.groupingBy(PlannedProjectPOJO::getId));

Wie kann ich das machen?

35
haisi

Die Verwendung von Collectors.groupingBy ist der richtige Ansatz. Statt jedoch die Version mit einem Argument zu verwenden, die eine Liste aller Elemente für jede Gruppe erstellt, sollten Sie die beiden Argumente version verwenden, die eine weitere Variable Collector verwendet, die bestimmt, wie die Elemente jeder Gruppe zusammengefasst werden .

Dies ist besonders reibungslos, wenn Sie eine einzelne Eigenschaft der Elemente zusammenfassen oder einfach die Anzahl der Elemente pro Gruppe zählen möchten:

  • Zählen:

    list.stream()
      .collect(Collectors.groupingBy(foo -> foo.id, Collectors.counting()))
      .forEach((id,count)->System.out.println(id+"\t"+count));
    
  • Eine Eigenschaft zusammenfassen:

    list.stream()
      .collect(Collectors.groupingBy(foo -> foo.id,
                                        Collectors.summingInt(foo->foo.targetCost)))
      .forEach((id,sumTargetCost)->System.out.println(id+"\t"+sumTargetCost));
    

In Ihrem Fall, wenn Sie mehr als eine Eigenschaft zusammenfassen möchten, die eine benutzerdefinierte Reduzierungsoperation angibt, wie in dieser Antwort vorgeschlagen, ist dies der richtige Ansatz. Sie können die Reduzierungsfunktion jedoch während der Gruppierungsoperation ausführen, so dass kein Sammeln erforderlich ist die gesamten Daten in einen Map<…,List>, bevor die Reduktion durchgeführt wird:

(Ich gehe davon aus, dass Sie jetzt einen import static Java.util.stream.Collectors.*; verwenden ...)

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

Der Vollständigkeit halber hier eine Lösung für ein Problem, das über den Rahmen Ihrer Frage hinausgeht: Was ist, wenn Sie mehrere Spalten/Eigenschaften GROUP BY möchten?

Das erste, was den Programmierern in den Sinn kommt, ist die Verwendung von groupingBy, um die Eigenschaften der Stream-Elemente zu extrahieren und ein neues Schlüsselobjekt zu erstellen/zurückzugeben. Dies erfordert jedoch eine geeignete Inhaberklasse für die Schlüsseleigenschaften (und Java hat keine allgemeine Tuple-Klasse).

Es gibt aber eine Alternative. Mit der three-arg-Form von groupingBy können wir einen Lieferanten für die tatsächliche Map-Implementierung angeben, die die Schlüsselgleichheit bestimmt. Durch die Verwendung einer sortierten Karte mit einem Vergleicher, der mehrere Eigenschaften miteinander vergleicht, erhalten wir das gewünschte Verhalten, ohne dass eine zusätzliche Klasse erforderlich ist. Wir müssen nur darauf achten, keine Eigenschaften aus den Schlüsselinstanzen zu verwenden, die unser Vergleicher ignoriert hat, da sie nur willkürliche Werte haben werden:

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

Hier ist ein möglicher Ansatz:

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

Ausgabe :

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

verwenden Sie einfach toMap (), sehr einfach 

4
user1241671

Dies ist nur mit der Stream-API des JDK möglich, wie andere Antworten gezeigt haben. In diesem Artikel wird erläutert, wie Sie die SQL-Semantik von GROUP BY in Java 8 (mit standardmäßigen Aggregatfunktionen) und mithilfe von jOOλ , einer Bibliothek, die Stream für diese Anwendungsfälle erweitert.

Schreiben:

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

Berufung

System.out.println(list);

Werde dann nachgeben

[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