web-dev-qa-db-fra.com

Groupe de flux Java par et additionner plusieurs champs

J'ai une liste de fooList

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

    ... constructor, getters & setters
}

Je voudrais regrouper par catégorie et ensuite additionner le montant ainsi que le prix.

Le résultat sera stocké dans une carte: 

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

La clé est le Foo qui détient le montant et le prix récapitulés, avec une liste comme valeur pour tous les objets de la même catégorie.

Jusqu'à présent, j'ai essayé ce qui suit:

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

Il ne me reste plus qu'à remplacer la clé String par un objet Foo contenant le montant et le prix récapitulatifs. Voici où je suis coincé. Je n'arrive pas à trouver un moyen de le faire.

8
MatMat

Un peu moche, mais ça devrait marcher:

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

Ma variante de Sweepers answer utilise un collecteur réducteur au lieu de diffuser deux fois en continu pour additionner les champs individuels:

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

Ce n’est pas vraiment meilleur cependant, car cela crée beaucoup de Foos de courte durée.

Notez cependant que Foo est nécessaire pour fournir les implémentations hashCode- et equals- qui prennent uniquement en compte category pour que la map résultante fonctionne correctement. Ce ne serait probablement pas ce que vous voulez pour Foos en général. Je préférerais définir une classe FooSummary distincte pour contenir les données agrégées. 

4
Hulk

Si vous avez un constructeur spécial dédié et des méthodes hashCode et equals implémentées de manière cohérente dans Foo comme suit:

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

Les implémentations hashCode et equals ci-dessus vous permettent d'utiliser Foo en tant que clé significative de la carte (sinon, votre carte serait brisée).

Maintenant, avec l'aide d'une nouvelle méthode dans Foo qui effectue l'agrégation des attributs amount et price en même temps, vous pouvez faire ce que vous voulez en 2 étapes. D'abord la méthode:

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

Maintenant la solution finale:

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: quelques observations manquaient ...

D'une part, cette solution vous oblige à utiliser une implémentation de hashCode et equals qui considère deux instances Foo différentes égales si elles appartiennent à la même category. Cela n’est peut-être pas souhaitable ou vous avez déjà une implémentation qui prend en compte davantage d’attributs ou d’autres attributs. 

D'un autre côté, utiliser Foo comme clé d'une carte utilisée pour regrouper des instances en fonction de l'un de ses attributs est un cas d'utilisation peu commun. Je pense qu'il serait préférable d'utiliser l'attribut category pour grouper par catégorie et disposer de deux mappes: Map<String, List<Foo>> pour conserver les groupes et Map<String, Foo> pour conserver les price et amount agrégées, la clé étant category dans les deux cas.

En plus de cela, cette solution mute les clés de la carte après y avoir inséré les entrées. Ceci est dangereux, car cela pourrait casser la carte. Cependant, ici, je ne modifie que les attributs de Foo qui ne participent ni à l'implémentation de hashCode ni de equalsFoo. Je pense que ce risque est acceptable dans ce cas, en raison du caractère inhabituel de l'exigence.

Je vous suggère de créer une classe d'assistance, qui tiendra le montant et le prix

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

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

Et puis juste collecter la liste pour mapper:

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

Mon point de vue sur une solution :)

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