4
votes

Utilisez java stream pour regrouper par 2 clés sur le même type

En utilisant java stream, comment créer une carte à partir d'une liste à indexer par 2 clés sur la même classe?

Je donne ici un code Exemple, je voudrais que la carte "personByName" récupère toute personne par firstName OU lastName, donc je voudrais obtenir les 3 "steves": quand c'est leur prénom ou nom. Je ne sais pas comment mélanger les 2 Collectors.groupingBy.

public static class Person {
    final String firstName;
    final String lastName;

    protected Person(String firstName, String lastName) {
        super();
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

}

@Test
public void testStream() {
    List<Person> persons = Arrays.asList(
            new Person("Bill", "Gates"),
            new Person("Bill", "Steve"),
            new Person("Steve", "Jobs"),
            new Person("Steve", "Wozniac"));

    Map<String, Set<Person>> personByFirstName = persons.stream().collect(Collectors.groupingBy(Person::getFirstName, Collectors.toSet()));
    Map<String, Set<Person>> personByLastName = persons.stream().collect(Collectors.groupingBy(Person::getLastName, Collectors.toSet()));

    Map<String, Set<Person>> personByName = persons.stream().collect(Collectors.groupingBy(Person::getLastName, Collectors.toSet()));// This is wrong, I want bot first and last name

    Assert.assertEquals("we should search by firstName AND lastName", 3, personByName.get("Steve").size()); // This fails

}

J'ai trouvé une solution de contournement en boucle sur les 2 cartes, mais ce n'est pas orienté flux. p>


0 commentaires

7 Réponses :


7
votes

Vous pouvez le faire comme ceci:

Steve=[Steve Wozniac, Bill Steve, Steve Jobs]
Jobs=[Steve Jobs]
Bill=[Bill Steve, Bill Gates]
Wozniac=[Steve Wozniac]
Gates=[Bill Gates]

En supposant que vous ajoutiez une méthode toString () à la classe Person , vous pouvez puis voir le résultat en utilisant:

List<Person> persons = Arrays.asList(
        new Person("Bill", "Gates"),
        new Person("Bill", "Steve"),
        new Person("Steve", "Jobs"),
        new Person("Steve", "Wozniac"));

// code above here

personByName.entrySet().forEach(System.out::println);

Sortie

Map<String, Set<Person>> personByName = persons.stream()
       .flatMap(p -> Stream.of(new SimpleEntry<>(p.getFirstName(), p),
                               new SimpleEntry<>(p.getLastName(), p)))
       .collect(Collectors.groupingBy(SimpleEntry::getKey,
                   Collectors.mapping(SimpleEntry::getValue, Collectors.toSet())));


0 commentaires

3
votes

Vous pouvez fusionner les deux Map > par exemple

Map<String, Set<Person>> personByFirstName = 
                            persons.stream()
                                   .collect(Collectors.groupingBy(
                                                   Person::getFirstName, 
                                                   Collectors.toCollection(HashSet::new))
                                           );

persons.stream()
       .collect(Collectors.groupingBy(Person::getLastName, Collectors.toSet()))
       .forEach((str, set) -> personByFirstName.merge(str, set, (s1, s2) -> { 
            s1.addAll(s2); 
            return s1;
        }));

// personByFirstName contains now all personByName


4 commentaires

Bonne et simple réponse, j'ajoute pour la modifier un peu pour la faire compiler, j'espère que cela ne vous dérange pas.


@pdem Heureux que cela puisse aider, c'est vrai que c'est plus lisible, mais je pense que la réponse d'Andreas permettra de meilleures performances, mais je ne sais pas si c'est quelque chose que vous voulez / avez besoin et je ne sais pas si c'est vrai


Sachez que Collectors.toSet () ne fournit "aucune garantie sur la mutabilité de l'ensemble retourné", donc s1.addAll (s2) peut jeter < code> UnsupportedOperationException (ou similaire) si l'ensemble renvoyé est immuable. Même si cela fonctionne maintenant, c'est une bombe qui attend de se déclencher dans le futur, si toSet () est changé. --- Pour corriger, changez d'abord toSet () en toCollection (HashSet :: new) , comme suggéré dans le javadoc de toSet () .


@Andreas merci je l'ai corrigé, c'est toujours bon à savoir! Mais je pense que ce serait le problème avec toute utilisation de toSet ()



0
votes

Si vous voulez une vraie solution orientée flux, assurez-vous de ne pas produire de grandes collections intermédiaires, sinon l'essentiel du sens des flux est perdu.

Si vous voulez simplement filtrer tous les Steves, filtrez d'abord , collectez plus tard:

persons.stream
  .filter(p -> p.getFirstName().equals('Steve') || p.getLastName.equals('Steve'))
  .collect(toList());

Si vous voulez faire des choses complexes avec un élément de flux, par exemple mettez un élément dans plusieurs collections, ou dans une carte sous plusieurs clés, consommez simplement un flux en utilisant forEach , et écrivez-y la logique de gestion que vous voulez.


2 commentaires

J'aurai des milliers de résultats sur des données statiques, pour une seule construction de carte. Un accès à la carte est O (1) et un accès au filtre est O (n). Je peux accepter que la constuction d'indice soit O (n.ln (n)) ou O (n²). un filtre n'est pas la solution pour mon cas, j'aurais pu le préciser dans la question.


Ensuite, créez simplement des cartes et utilisez-les, c'est tout à fait logique! Vous n'avez pas besoin de la mécanique de flux qui a un ensemble de compromis très différent.



0
votes

Vous ne pouvez pas saisir vos cartes par plusieurs valeurs. Pour ce que vous voulez réaliser, vous avez trois options:

  1. Combinez vos cartes "personByFirstName" et "personByLastName", vous aurez des valeurs en double (par exemple, Bill Gates sera dans la carte sous la clé Bill et également dans la carte sous le touche Gates ). @Andreas answer est un bon moyen de le faire en fonction du flux.

  2. Utilisez une bibliothèque d'indexation comme lucene et indexez tous vos objets Person par prénom et nom.

  3. L'approche par flux - elle ne sera pas performante sur de grands ensembles de données, mais vous pouvez diffuser votre collection et utiliser filter pour obtenir vos correspondances:

persons
    .stream()
    .filter(p -> p.getFirstName().equals("Steve") 
         || p.getLastName().equals("Steve"))
    .collect(Collectors.asList());

(J'ai écrit la syntaxe de mémoire, vous devrez peut-être la modifier).


0 commentaires

0
votes

Si j'ai bien compris, vous voulez mapper chaque personne deux fois, une fois pour le prénom et une fois pour la dernière. Pour ce faire, vous devez doubler votre flux d'une manière ou d'une autre. En supposant que Couple soit un 2-tuple existant (Guava ou Vavr ont une implémentation intéressante), vous pourriez:

persons.stream()
    .map(p -> new Couple(new Couple(p.firstName, p), new Couple(p.lastName, p)))
    .flatMap(c -> Stream.of(c.left, c.right)) // Stream of Couple(String, Person)
    .map(c -> new Couple(c.left, Arrays.asList(c.right)))
    .collect(Collectors.toMap(Couple::getLeft, Couple::getRight, Collection::addAll));

Je ne l'ai pas testé, mais le concept est: créer un flux de (nom, personne), (nom, personne) ... pour chaque personne, puis cartographiez simplement la valeur de gauche de chaque couple. L'asList doit avoir une collection comme valeur. Si vous avez besoin d'un Set, suivez la dernière ligne avec .collect (Collectors.toMap (Couple :: getLeft, c -> new HashSet (c.getRight), Collection :: addAll))


0 commentaires

3
votes

Une solution consisterait à utiliser le dernier JDK12 Collector.teeing a >:

Map<String, Set<Person>> result = persons.stream().
       .collect(Collectors.teeing(
                Collectors.groupingBy(Person::getFirstName, 
                                      Collectors.toCollection(LinkedHashSet::new)),
                Collectors.groupingBy(Person::getLastName, Collectors.toSet()),
                (byFirst, byLast) -> { 
                    byLast.forEach((last, peopleSet) -> 
                           byFirst.computeIfAbsent(last, k -> new LinkedHashSet<>())
                                  .addAll(peopleSet));
                    return byFirst; 
                }));

Collectors.teeing collecte vers deux collecteurs distincts, puis fusionne les résultats en une valeur finale. À partir de la documentation:

Renvoie un collecteur qui est un composite de deux collecteurs en aval. Chaque élément passé au collecteur résultant est traité par les deux collecteurs en aval, puis leurs résultats sont fusionnés à l'aide de la fonction de fusion spécifiée dans le résultat final.

Ainsi, le code ci-dessus est collecté sur une carte par prénom et également sur une carte par nom de famille, puis fusionne les deux cartes en une carte finale en itérant la carte byLast et en fusionnant chacune de ses entrées dans la carte byFirst au moyen de méthode Map.computeIfAbsent . Enfin, la carte byFirst est renvoyée.

Notez que j'ai collecté vers un Map > au lieu de vers un Map > code > pour garder l'exemple simple. Si vous avez réellement besoin d'une carte d'ensembles, vous pouvez le faire comme suit:

Map<String, List<Person>> result = persons.stream()
       .collect(Collectors.teeing(
                Collectors.groupingBy(Person::getFirstName, 
                                      Collectors.toCollection(ArrayList::new)),
                Collectors.groupingBy(Person::getLastName),
                (byFirst, byLast) -> { 
                    byLast.forEach((last, peopleList) -> 
                           byFirst.computeIfAbsent(last, k -> new ArrayList<>())
                                  .addAll(peopleList));
                    return byFirst; 
                }));

Gardez à l'esprit que si vous avez besoin d'avoir Set code > comme valeurs des cartes, la classe Person doit implémenter les méthodes hashCode et equals de manière cohérente . p>


1 commentaires

C'est une très belle solution!



0
votes

Essayez SetMultimap depuis Google Guava ou ma bibliothèque Abacus-Util

SetMultimap<String, Person> result = Multimaps.newSetMultimap(new HashMap<>(), () -> new HashSet<>()); // by Google Guava.
// Or result = N.newSetMultimap(); // By Abacus-Util
persons.forEach(p -> {
     result.put(p.getFirstName(), p);
     result.put(p.getLastName(), p);
  });


0 commentaires