Java8: HashMap à HashMap à l’aide de Stream / Map-Reduce / Collector

Je sais “transformer” une simple List Java à partir de Y -> Z , à savoir:

 List x; List y = x.stream() .map(s -> Integer.parseInt(s)) .collect(Collectors.toList()); 

Maintenant, j’aimerais faire la même chose avec une carte, à savoir:

 INPUT: { "key1" -> "41", // "41" and "42" "key2" -> "42 // are Ssortingngs } OUTPUT: { "key1" -> 41, // 41 and 42 "key2" -> 42 // are Integers } 

La solution ne doit pas être limitée à Ssortingng -> Integer . Tout comme dans l’exemple ci-dessus, je voudrais appeler n’importe quelle méthode (ou constructeur).

 Map x; Map y = x.entrySet().stream() .collect(Collectors.toMap( e -> e.getKey(), e -> Integer.parseInt(e.getValue()) )); 

Ce n’est pas aussi beau que le code de la liste. Vous ne pouvez pas construire de nouvelles Map.Entry s dans un appel map() pour que le travail soit mélangé à l’appel collect() .

Voici quelques variantes de la réponse de Sotirios Delimanolis , qui était plutôt bonne pour commencer (+1). Considérer ce qui suit:

 static  Map transform(Map input, Function function) { return input.keySet().stream() .collect(Collectors.toMap(Function.identity(), key -> function.apply(input.get(key)))); } 

Quelques points ici. La première est l’utilisation de caractères génériques dans les génériques; Cela rend la fonction un peu plus flexible. Un caractère générique serait nécessaire si, par exemple, vous souhaitiez que la carte de sortie ait une clé qui soit une super-classe de la clé de la carte en entrée:

 Map input = new HashMap(); input.put("ssortingng1", "42"); input.put("ssortingng2", "41"); Map output = transform(input, Integer::parseInt); 

(Il y a aussi un exemple pour les valeurs de la carte, mais c’est vraiment artificiel, et j’avoue que le fait d’avoir un caractère générique limité pour Y aide uniquement dans les cas limites.)

Un second point est que, au lieu d’exécuter le stream sur le entrySet la carte en entrySet , je l’ai exécuté sur le keySet . Je pense que cela rend le code un peu plus propre, au prix de devoir extraire des valeurs de la carte au lieu de l’entrer dans la carte. Incidemment, j’avais initialement key -> key comme premier argument de toMap() et cela a échoué avec une erreur d’inférence de type pour une raison quelconque. En changeant la (X key) -> key fonctionné, tout comme Function.identity() .

Une autre variante est la suivante:

 static  Map transform1(Map input, Function function) { Map result = new HashMap<>(); input.forEach((k, v) -> result.put(k, function.apply(v))); return result; } 

Cela utilise Map.forEach() au lieu de stream. Je pense que cela est encore plus simple car cela évite aux collectionneurs, qui sont un peu maladroits d’utiliser des cartes. La raison en est que Map.forEach() donne la clé et la valeur en tant que parameters séparés, alors que le stream ne possède qu’une valeur – et que vous devez choisir d’utiliser la clé ou l’entrée de la carte comme valeur. Du côté des moins, cela manque de la bonté riche et fluide des autres approches. 🙂

Une solution générique comme ça

 public static  Map transform(Map input, Function function) { return input .entrySet() .stream() .collect( Collectors.toMap((entry) -> entry.getKey(), (entry) -> function.apply(entry.getValue()))); } 

Exemple

 Map input = new HashMap(); input.put("ssortingng1", "42"); input.put("ssortingng2", "41"); Map output = transform(input, (val) -> Integer.parseInt(val)); 

Doit-il absolument être 100% fonctionnel et fluide? Sinon, que diriez-vous de cela, qui est à peu près aussi court:

 Map output = new HashMap<>(); input.forEach((k, v) -> output.put(k, Integer.valueOf(v)); 

( si vous pouvez vivre avec la honte et la culpabilité de combiner des stream avec des effets secondaires )

Ma bibliothèque StreamEx qui améliore l’API de stream standard fournit une classe EntryStream qui convient mieux à la transformation de cartes:

 Map output = EntryStream.of(input).mapValues(Integer::valueOf).toMap(); 

La fonction de goyave Maps.transformValues est ce que vous recherchez, et elle fonctionne bien avec les expressions lambda:

 Maps.transformValues(originalMap, val -> ...) 

Si cela ne vous dérange pas d’utiliser des bibliothèques tierces, ma lib cyclops- react a des extensions pour tous les types de collections JDK , y compris Map . Nous pouvons simplement transformer la carte directement en utilisant l’opérateur ‘map’ (par défaut, la carte agit sur les valeurs de la carte).

  MapX y = MapX.fromMap(HashMaps.of("hello","1")) .map(Integer::parseInt); 

bimap peut être utilisé pour transformer les clés et les valeurs en même temps

  MapX y = MapX.fromMap(HashMaps.of("hello","1")) .bimap(this::newKey,Integer::parseInt); 

Une alternative qui existe toujours pour l’apprentissage est de construire votre collecteur personnalisé via Collector.of () bien que le collecteur JDK de toMap () soit succinct (+1 ici ).

 Map newMap = givenMap. entrySet(). stream().collect(Collector.of ( ()-> new HashMap(), (mutableMap,entryItem)-> mutableMap.put(entryItem.getKey(),Integer.parseInt(entryItem.getValue())), (map1,map2)->{ map1.putAll(map2); return map1;} )); 

La solution déclarative et plus simple serait:

yourMutableMap.replaceAll ((clé, val) -> return_value_of_bi_your_function); Nb. Sachez que vous modifiez l’état de votre carte. Donc, ce n’est peut-être pas ce que vous voulez.

Bravo à: http://www.deadcoderising.com/2017-02-14-java-8-declarative-ways-of-modifying-a-map-using-compute-merge-and-replace/