2017-05-17 2 views
3

J'ai donc ce code qui « fonctionne » (remplacer certains noms pour la simplicité):En utilisant groupingBy dans une carte imbriquée, mais la collecte à un autre type d'objet

Map<String, Map<String, ImmutableList<SomeClassA>>> someMap = 
     someListOfClassA.stream() 
     .filter(...) 
     .collect(Collectors.groupingBy(SomeClassA::someCriteriaA, 
      Collectors.groupingBy(SomeClassA::someCriteriaB, GuavaCollectors.toImmutableList() 
      ) 
    )); 

Cependant, je veux changer ce code de sorte que la collection interne est de SomeClassB après le regroupement par des champs SomeClassA. Par exemple, si les classes ressemblent à ceci:

en supposant qu'ils ont tous les deux tous args constructeurs

class SomeClassA { 
    String someCriteriaA; 
    String someCriteriaB; 
    T someData; 
    String someId; 
} 

class SomeClassB { 
    T someData; 
    String someId; 
} 

Et il y a une méthode quelque part:

public static Collection<SomeClassB> getSomeClassBsFromSomeClassA(SomeClassA someA) { 
    List<Some List of Class B> listOfB = someMethod(someA); 
    return listOfB; // calls something using someClassA, gets a list of SomeClassB 
} 

Je veux aplatir les listes résultant de SomeClass Bs dans

Map<String, Map<String, ImmutableList<SomeClassB>>> someMap = 
    someListOfClassA.stream() 
    .filter(...) 
    . // not sure how to group by SomeClassA fields but result in a list of SomeClassB since one SomeClassA can result in multiple SomeClassB 

Je ne sais pas comment cela rentrerait dans le code abov e. Comment puis-je recueillir un ensemble de listes basées sur SomeClassB dans une seule liste pour toutes les valeurs de SomeClassA? Si un seul ClassA correspond à un ClassB unique, je sais comment le faire fonctionner en utilisant Collectors.mapping mais comme chaque ClassA donne plusieurs ClassBs, je ne sais pas comment le faire fonctionner.

Toutes les idées seraient appréciées. Merci!

+6

En Java 9 vous aurez [ 'Collectors.flatMapping()'] (http://download.java.net/java/jdk9/ docs/api/java/util/flux/Collectors.html # flatMapping-java.util.function.Function-java.util.stream.Collector-), ce qui vous permettra d'aplatir les résultats en aval du regroupement. – shmosel

Répondre

4

Avec un collecteur personnalisé comme ceci:

private static Collector<Collection<SomeClassB>, ?, ImmutableList<SomeClassB>> 
     flatMapToImmutableList() { 
     return Collectors.collectingAndThen(Collectors.toList(), 
       listOfCollectionsOfB -> 
         listOfCollectionsOfB.stream() 
           .flatMap(Collection::stream) 
           .collect(GuavaCollectors.toImmutableList())); 
    } 

vous pouvez réaliser ce que vous êtes après:

Map<String, Map<String, List<SomeClassB>>> someMap = 
       someListOfClassA.stream() 
         .filter(...) 
         .collect(Collectors.groupingBy(SomeClassA::getSomeCriteriaA, 
           Collectors.groupingBy(SomeClassA::getSomeCriteriaB, 
             Collectors.mapping(a -> getSomeClassBsFromSomeClassA(a), 
               flatMapToImmutableList())))); 
3

Alors que nous attendons tous Java 9 de Collectors.flatMapping (grâce à @shmosel pour le lien), vous pouvez écrire votre propre collection pour obtenir ce que vous voulez:

public static <T, D, R> Collector<T, ?, R> flatMapping(
     Function<? super T, ? extends Stream<? extends D>> streamMapper, 
     Collector<? super D, ?, R> downstream) { 

    class Acc { 
     Stream.Builder<Stream<? extends D>> builder = Stream.builder(); 

     void add(T t) { 
      builder.accept(streamMapper.apply(t)); 
     } 

     Acc combine(Acc another) { 
      another.builder.build().forEach(builder); 
      return this; 
     } 

     R finish() { 
      return builder.build() 
        .flatMap(Function.identity()) // Here! 
        .collect(downstream); 
     } 
    } 
    return Collector.of(Acc::new, Acc::add, Acc::combine, Acc::finish); 
} 

Cette méthode helper utilisation Collector.of et une classe locale Acc pour accumuler les flux renvoyés par la fonction streamMapper fournie, qui prend comme argument un élément du flux d'origine. Ces flux sont accumulés dans un Stream.Builder qui sera construit lorsque la fonction de finition du collecteur est appliquée.

Immédiatement après la construction de ce flux de flux, il est mappé à plat avec la fonction d'identité, puisque nous voulons seulement concaténer les flux. (J'aurais pu utiliser une liste de flux au lieu d'un flux de flux, mais je pense que Stream.Builder est à la fois très efficace et très sous-utilisé).

Acc met également en œuvre une méthode de combinaison qui fusionnera les « flux de de flux dans ce Acc » Acc donné constructeur de flux de. Cette fonctionnalité ne sera utilisée que si le flux d'origine est parallèle.

Voici comment utiliser cette méthode avec votre exemple:

Map<String, Map<String, ImmutableList<SomeClassB>>> map = someListOfClassA.stream() 
    .filter(...) 
    .collect(
     Collectors.groupingBy(SomeClassA::getSomeCriteriaA, 
      Collectors.groupingBy(SomeClassA::getSomeCriteriaB, 
       flatMapping(
        a -> getSomeClassBsFromSomeClassA(a).stream(), 
        ImmutableList.toImmutableList())))); 

EDIT: Comme @Holger indique dans les commentaires ci-dessous, il n'y a pas besoin de données de mise en mémoire tampon dans un générateur de flux lors de l'accumulation. Au lieu de cela, un collecteur de cartographie plate peut être implémenté en effectuant l'aplatissement à droite dans la fonction d'accumulateur.Here is @Holger's own implementation of such collector, que je copie ici textuellement avec son consentement:

public 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])); 
} 
+1

Il n'est pas nécessaire de stocker des données dans un générateur. Un collecteur 'flatMapping' peut être implémenté en effectuant l'aplatissement à droite dans la fonction accumulateur. À la fin de [cette réponse] (http://stackoverflow.com/a/39131049/2711488) est une telle implémentation. Ce n'est pas seulement plus efficace, mais plus simple tout en se souciant de la politique de fermeture. C'est sur le pair (sinon identique) à l'implémentation de Java 9. – Holger

+0

@Holger Vous avez tout à fait raison, dans votre version, le mapping plat est implicitement exécuté lorsque le flux est terminé (en accumulant ses éléments dans le collecteur aval). Me permettriez-vous de relier votre réponse à la mienne et de copier votre collectionneur de cartographie à plat verbatim, en faisant comprendre au lecteur que le collectionneur est le vôtre? –