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>
7 Réponses :
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())));
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
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 ()
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.
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.
Vous ne pouvez pas saisir vos cartes par plusieurs valeurs. Pour ce que vous voulez réaliser, vous avez trois options:
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.
Utilisez une bibliothèque d'indexation comme lucene et indexez tous vos objets Person par prénom et nom.
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).
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))
Une solution consisterait à utiliser le dernier JDK12 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 Notez que j'ai collecté vers un Gardez à l'esprit que si vous avez besoin d'avoir 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:
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. Map
au lieu de vers un Map
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;
}));
Set
Person
doit implémenter les méthodes hashCode
et equals
de manière cohérente . p>
C'est une très belle solution!
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); });