web-dev-qa-db-fra.com

Transformer et filtrer une Java Carte avec des flux

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?

35
Paul I

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();
41
Tunaki

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.

11
Holger

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.

5
shmosel

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 oldMap est un IdentityHashMap et que mapFactory crée un HashMap.

3
Alex

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.

0
user_3380739