web-dev-qa-db-fra.com

Java 8 Nested (Multi level) group by

J'ai quelques cours comme ci-dessous

class Pojo {
    List<Item> items;
}

class Item {
    T key1;
    List<SubItem> subItems;
}

class SubItem {
    V key2;
    Object otherAttribute1;
}

Je souhaite agréger les éléments en fonction de key1 et pour chaque agrégation, les sous-éléments doivent être agrégés par key2 de la manière suivante:

Map<T, Map<V, List<Subitem>>

Comment est-ce possible avec Java 8 Collectors.groupingBy imbrication?

J'essayais quelque chose et je suis resté à mi-chemin

pojo.getItems()
    .stream()
    .collect(
        Collectors.groupingBy(Item::getKey1, /* How to group by here SubItem::getKey2*/)
    );

Remarque: Ce n'est pas la même chose que groupingBy en cascade qui fait une agrégation à plusieurs niveaux basée sur des champs dans le même objet comme indiqué ici

16
Sandesh Kumar

Vous ne pouvez pas grouper un seul élément par plusieurs clés, sauf si vous acceptez que l'élément puisse apparaître dans plusieurs groupes. Dans ce cas, vous souhaitez effectuer une sorte d'opération flatMap.

Une façon d'y parvenir consiste à utiliser Stream.flatMap avec une paire temporaire contenant les combinaisons de Item et SubItem avant la collecte. En raison de l'absence d'un type de paire standard, une solution typique consiste à utiliser Map.Entry pour ça:

Map<T, Map<V, List<SubItem>>> result = pojo.getItems().stream()
    .flatMap(item -> item.subItems.stream()
        .map(sub -> new AbstractMap.SimpleImmutableEntry<>(item, sub)))
    .collect(Collectors.groupingBy(e -> e.getKey().getKey1(),
                Collectors.mapping(Map.Entry::getValue,
                    Collectors.groupingBy(SubItem::getKey2))));

Une alternative ne nécessitant pas ces objets temporaires serait d'effectuer l'opération flatMap directement dans le collecteur, mais malheureusement, flatMapping ne sera pas disponible avant Java 9.

Avec cela, la solution ressemblerait à

Map<T, Map<V, List<SubItem>>> result = pojo.getItems().stream()
    .collect(Collectors.groupingBy(Item::getKey1,
                Collectors.flatMapping(item -> item.getSubItems().stream(),
                    Collectors.groupingBy(SubItem::getKey2))));

et si nous ne voulons pas attendre Java 9 pour cela, nous pouvons ajouter un collecteur similaire à notre base de code, car ce n'est pas si difficile à implémenter:

static <T,U,A,R> Collector<T,?,R> flatMapping(
    Function<? super T,? extends Stream<? extends U>> mapper,
    Collector<? super U,A,R> downstream) {

    BiConsumer<A, ? super U> acc = downstream.accumulator();
    return Collector.of(downstream.supplier(),
        (a, t) -> { try(Stream<? extends U> s=mapper.apply(t)) {
            if(s!=null) s.forEachOrdered(u -> acc.accept(a, u));
        }},
        downstream.combiner(), downstream.finisher(),
        downstream.characteristics().toArray(new Collector.Characteristics[0]));
}
22
Holger