it-swarm.com.de

Java-Streamgruppe nach und Summe mehrerer Felder

Ich habe eine Liste fooList

class Foo {
    private String category;
    private int amount;
    private int price;

    ... constructor, getters & setters
}

Ich möchte nach Kategorien gruppieren und dann Betrag sowie Preis summieren.

Das Ergebnis wird in einer Karte gespeichert: 

Map<Foo, List<Foo>> map = new HashMap<>();

Der Schlüssel ist der Foo, der den zusammenfassenden Betrag und den Preis enthält, mit einer Liste für alle Objekte mit derselben Kategorie.

Bisher habe ich folgendes ausprobiert:

Map<String, List<Foo>> map = fooList.stream().collect(groupingBy(Foo::getCategory()));

Jetzt muss ich nur noch den String-Schlüssel durch ein Foo-Objekt ersetzen, das die Summe und den Preis enthält. Hier stecke ich fest. Ich kann keinen Weg finden, dies zu tun.

8
MatMat

Ein bisschen hässlich, aber es sollte funktionieren:

list.stream().collect(Collectors.groupingBy(Foo::getCategory))
    .entrySet().stream()
    .collect(Collectors.toMap(x -> {
        int sumAmount = x.getValue().stream().mapToInt(Foo::getAmount).sum();
        int sumPrice= x.getValue().stream().mapToInt(Foo::getPrice).sum();
        return new Foo(x.getKey(), sumAmount, sumPrice);
    }, Map.Entry::getValue));
12
Sweeper

Meine Variation von Sweepers answer verwendet einen reduzierenden Collector, anstatt zweimal zu streamen, um die einzelnen Felder zu summieren:

        Map<Foo, List<Foo>> map = fooList.stream()
                .collect(Collectors.groupingBy(Foo::getCategory))
                .entrySet().stream()
                .collect(Collectors.toMap(e -> e.getValue().stream().collect(
                            Collectors.reducing((l, r) -> new Foo(l.getCategory(),
                                        l.getAmount() + r.getAmount(),
                                        l.getPrice() + r.getPrice())))
                            .get(), 
                            e -> e.getValue()));

Es ist nicht wirklich better, da es eine Menge kurzlebiger Foos erzeugt.

Beachten Sie jedoch, dass Foo erforderlich ist, um hashCode- und equals-Implementierungen bereitzustellen, die nur category berücksichtigen, damit die resultierende map ordnungsgemäß funktioniert. Dies ist wahrscheinlich nicht das, was Sie für Foos im Allgemeinen wünschen. Ich würde es vorziehen, eine eigene FooSummary-Klasse zu definieren, die die aggregierten Daten enthält. 

4
Hulk

Wenn Sie einen speziellen, dedizierten Konstruktor und hashCode- und equals-Methoden haben, die in Foo wie folgt konsistent implementiert sind:

public Foo(Foo that) { // not a copy constructor!!!
    this.category = that.category;
    this.amount = 0;
    this.price = 0;
}

public int hashCode() {
    return Objects.hashCode(category);
}

public boolean equals(Object another) {
   if (another == this) return true;
   if (!(another instanceof Foo)) return false;
   Foo that = (Foo) another;
   return Objects.equals(this.category, that.category);
}

Die Implementierungen hashCode und equals ermöglichen Ihnen die Verwendung von Foo als sinnvollen Schlüssel in der Karte (andernfalls würde Ihre Karte beschädigt werden).

Mit Hilfe einer neuen Methode in Foo, die gleichzeitig die Aggregation der amount- und price-Attribute durchführt, können Sie in zwei Schritten tun, was Sie möchten. Zuerst die Methode:

public void aggregate(Foo that) {
    this.amount += that.amount;
    this.price += that.price;
}

Nun die endgültige Lösung:

Map<Foo, List<Foo>> result = fooList.stream().collect(
    Collectors.collectingAndThen(
        Collectors.groupingBy(Foo::new), // works: special ctor, hashCode & equals
        m -> { m.forEach((k, v) -> v.forEach(k::aggregate)); return m; }));

EDIT: ein paar Beobachtungen fehlten ...

Einerseits zwingt Sie diese Lösung dazu, eine Implementierung von hashCode und equals zu verwenden, die zwei verschiedene Foo-Instanzen als gleich betrachtet, wenn sie zu derselben category gehören. Möglicherweise ist dies nicht erwünscht oder Sie haben bereits eine Implementierung, die mehr oder andere Attribute berücksichtigt. 

Andererseits ist die Verwendung von Foo als Schlüssel einer Map, die zum Gruppieren von Instanzen nach einem ihrer Attribute verwendet wird, ein ziemlich ungewöhnlicher Anwendungsfall. Ich denke, es wäre besser, das category-Attribut zu verwenden, um nach Kategorien zu gruppieren und zwei Karten zu haben: Map<String, List<Foo>>, um die Gruppen beizubehalten, und Map<String, Foo>, um die aggregierten price und amount beizubehalten, wobei der Schlüssel in beiden Fällen der category ist.

Außerdem verändert diese Lösung die Schlüssel der Karte, nachdem die Einträge eingefügt wurden. Dies ist gefährlich, weil dadurch die Karte beschädigt werden kann. Hier verändere ich jedoch nur Attribute von Foo, die weder an der Implementierung von hashCode noch an equalsFoo teilnehmen. Ich denke, dass dieses Risiko aufgrund der Ungewöhnlichkeit der Anforderung in diesem Fall akzeptabel ist.

Ich schlage vor, dass Sie eine Helfer-Klasse erstellen, die Betrag und Preis enthält

final class Pair {
    final int amount;
    final int price;

    Pair(int amount, int price) {
        this.amount = amount;
        this.price = price;
    }
}

Und dann einfach Liste zur Karte sammeln:

List<Foo> list =//....;

Map<Foo, Pair> categotyPrise = list.stream().collect(Collectors.toMap(foo -> foo,
                    foo -> new Pair(foo.getAmount(), foo.getPrice()),
                    (o, n) -> new Pair(o.amount + n.amount, o.price + n.price)));
1
dehasi

Meine nehmen eine lösung :)

public static void main(String[] args) {
    List<Foo> foos = new ArrayList<>();
    foos.add(new Foo("A", 1, 10));
    foos.add(new Foo("A", 2, 10));
    foos.add(new Foo("A", 3, 10));
    foos.add(new Foo("B", 1, 10));
    foos.add(new Foo("C", 1, 10));
    foos.add(new Foo("C", 5, 10));

    List<Foo> summarized = new ArrayList<>();
    Map<Foo, List<Foo>> collect = foos.stream().collect(Collectors.groupingBy(new Function<Foo, Foo>() {
        @Override
        public Foo apply(Foo t) {
            Optional<Foo> fOpt = summarized.stream().filter(e -> e.getCategory().equals(t.getCategory())).findFirst();
            Foo f;
            if (!fOpt.isPresent()) {
                f = new Foo(t.getCategory(), 0, 0);
                summarized.add(f);
            } else {
                f = fOpt.get();
            }
            f.setAmount(f.getAmount() + t.getAmount());
            f.setPrice(f.getPrice() + t.getPrice());
            return f;
        }
    }));
    System.out.println(collect);
}