Je souhaite transformer et filtrer une carte Java). Comme exemple trivial, supposons que je souhaite convertir toutes les valeurs en nombres entiers, puis supprimer les entrées impaires.
Map<String, String> input = new HashMap<>();
input.put("a", "1234");
input.put("b", "2345");
input.put("c", "3456");
input.put("d", "4567");
Map<String, Integer> output = input.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> Integer.parseInt(e.getValue())
))
.entrySet().stream()
.filter(e -> e.getValue() % 2 == 0)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
System.out.println(output.toString());
Ceci est correct et donne: {a=1234, c=3456}
Cependant, je ne peux pas m'empêcher de me demander s'il est possible d'éviter d'appeler deux fois .entrySet().stream()
.
Existe-t-il un moyen de réaliser à la fois des opérations de transformation et de filtrage et d'appeler .collect()
une seule fois à la fin?
Oui, vous pouvez mapper chaque entrée sur une autre entrée temporaire contenant la clé et la valeur entière analysée. Ensuite, vous pouvez filtrer chaque entrée en fonction de leur valeur.
Map<String, Integer> output =
input.entrySet()
.stream()
.map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), Integer.valueOf(e.getValue())))
.filter(e -> e.getValue() % 2 == 0)
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue
));
Notez que j'ai utilisé Integer.valueOf
au lieu de parseInt
puisque nous voulons en fait un int
en boîte.
Si vous avez le luxe d'utiliser la bibliothèque StreamEx , vous pouvez le faire tout simplement:
Map<String, Integer> output =
EntryStream.of(input).mapValues(Integer::valueOf).filterValues(v -> v % 2 == 0).toMap();
Une façon de résoudre le problème avec beaucoup moins de frais généraux consiste à déplacer la cartographie et le filtrage vers le collecteur.
Map<String, Integer> output = input.entrySet().stream().collect(
HashMap::new,
(map,e)->{ int i=Integer.parseInt(e.getValue()); if(i%2==0) map.put(e.getKey(), i); },
Map::putAll);
Cela ne nécessite pas la création d'instances intermédiaires Map.Entry
Et, mieux encore, reportera la mise en boîte des valeurs de int
au point où les valeurs sont réellement ajoutées à la Map
, qui implique que les valeurs rejetées par le filtre ne sont pas encadrées du tout.
Comparée à ce que fait Collectors.toMap(…)
, l’opération est également simplifiée en utilisant Map.put
Plutôt que Map.merge
, Car nous savons à l’avance que nous n’avons pas à gérer les collisions clés ici.
Cependant, aussi longtemps que vous ne voulez pas utiliser l’exécution parallèle, vous pouvez aussi considérer la boucle ordinaire.
HashMap<String,Integer> output=new HashMap<>();
for(Map.Entry<String, String> e: input.entrySet()) {
int i = Integer.parseInt(e.getValue());
if(i%2==0) output.put(e.getKey(), i);
}
ou la variante d'itération interne:
HashMap<String,Integer> output=new HashMap<>();
input.forEach((k,v)->{ int i = Integer.parseInt(v); if(i%2==0) output.put(k, i); });
ce dernier étant assez compact et au moins comparable à toutes les autres variantes en matière de performances mono-thread.
Guava c'est votre ami:
Map<String, Integer> output = Maps.filterValues(Maps.transformValues(input, Integer::valueOf), i -> i % 2 == 0);
N'oubliez pas que output
est une vue transformée et filtrée de input
. Vous devrez en faire une copie si vous souhaitez les utiliser indépendamment.
Vous pouvez utiliser la méthode Stream.collect(supplier, accumulator, combiner)
pour transformer les entrées et les accumuler de manière conditionnelle:
Map<String, Integer> even = input.entrySet().stream().collect(
HashMap::new,
(m, e) -> Optional.ofNullable(e)
.map(Map.Entry::getValue)
.map(Integer::valueOf)
.filter(i -> i % 2 == 0)
.ifPresent(i -> m.put(e.getKey(), i)),
Map::putAll);
System.out.println(even); // {a=1234, c=3456}
Ici, à l'intérieur de l'accumulateur, j'utilise les méthodes Optional
pour appliquer à la fois la transformation et le prédicat, et si la valeur facultative est toujours présente, je l'ajoute à la carte en cours de collecte.
Une autre façon de faire est de supprimer les valeurs que vous ne voulez pas du fichier transformé Map
:
Map<String, Integer> output = input.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> Integer.parseInt(e.getValue()),
(a, b) -> { throw new AssertionError(); },
HashMap::new
));
output.values().removeIf(v -> v % 2 != 0);
Cela suppose que vous voulez un Map
mutable comme résultat, sinon vous pouvez probablement en créer un immuable à partir de output
.
Si vous transformez les valeurs dans le même type et souhaitez modifier le Map
à la place, cela pourrait être beaucoup plus court avec replaceAll
:
input.replaceAll((k, v) -> v + " example");
input.values().removeIf(v -> v.length() > 10);
Cela suppose également que input
est modifiable.
Je ne le recommande pas car cela ne fonctionnera pas pour toutes les implémentations valides Map
et risque de ne plus fonctionner pour HashMap
, mais vous pouvez actuellement utiliser replaceAll
et le transtypage. un HashMap
pour changer le type des valeurs:
((Map)input).replaceAll((k, v) -> Integer.parseInt((String)v));
Map<String, Integer> output = (Map)input;
output.values().removeIf(v -> v % 2 != 0);
Cela vous donnera également des avertissements de sécurité de type et si vous essayez de récupérer une valeur à partir du Map
via une référence de type ancien comme ceci:
String ex = input.get("a");
Il lancera un ClassCastException
.
Vous pouvez déplacer la première pièce transformée dans une méthode permettant d'éviter le passe-partout si vous prévoyez de l'utiliser beaucoup:
public static <K, VO, VN, M extends Map<K, VN>> M transformValues(
Map<? extends K, ? extends VO> old,
Function<? super VO, ? extends VN> f,
Supplier<? extends M> mapFactory){
return old.entrySet().stream().collect(Collectors.toMap(
Entry::getKey,
e -> f.apply(e.getValue()),
(a, b) -> { throw new IllegalStateException("Duplicate keys for values " + a + " " + b); },
mapFactory));
}
Et utilisez-le comme ceci:
Map<String, Integer> output = transformValues(input, Integer::parseInt, HashMap::new);
output.values().removeIf(v -> v % 2 != 0);
Notez que l’exception de clé en double peut être levée si, par exemple, le old
Map
est un IdentityHashMap
et que mapFactory
crée un HashMap
.
Voici le code par AbacusUtil
Map<String, String> input = N.asMap("a", "1234", "b", "2345", "c", "3456", "d", "4567");
Map<String, Integer> output = Stream.of(input)
.groupBy(e -> e.getKey(), e -> N.asInt(e.getValue()))
.filter(e -> e.getValue() % 2 == 0)
.toMap(Map.Entry::getKey, Map.Entry::getValue);
N.println(output.toString());
Déclaration : Je suis le développeur de AbacusUtil.