Java8: HashMap <X, Y> à HashMap <X, Z> à l'aide de Stream / Map-Reduce / Collector

210

Je sais comment "transformer" un simple Java à Listpartir de Y-> Z, c'est-à-dire:

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

Maintenant, je voudrais faire la même chose avec une carte, c'est-à-dire:

INPUT:
{
  "key1" -> "41",    // "41" and "42"
  "key2" -> "42      // are Strings
}

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

La solution ne doit pas se limiter à String-> Integer. Tout comme dans l' Listexemple ci-dessus, je voudrais appeler n'importe quelle méthode (ou constructeur).

Benjamin M
la source

Réponses:

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

Ce n'est pas aussi agréable que le code de liste. Vous ne pouvez pas construire de nouveaux Map.Entrys dans un map()appel, donc le travail est mélangé à l' collect()appel.

John Kugelman
la source
59
Vous pouvez remplacer e -> e.getKey()par Map.Entry::getKey. Mais c'est une question de goût / style de programmation.
Holger
5
En fait, c'est une question de performance, votre suggestion étant légèrement supérieure au «style» lambda
Jon Burgin
36

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

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

Quelques points ici. Le premier 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 vouliez que la carte de sortie ait une clé qui est une superclasse de la clé de la carte d'entrée:

Map<String, String> input = new HashMap<String, String>();
input.put("string1", "42");
input.put("string2", "41");
Map<CharSequence, Integer> output = transform(input, Integer::parseInt);

(Il existe également un exemple pour les valeurs de la carte, mais il est vraiment artificiel, et j'avoue que le caractère générique borné pour Y n'aide que dans les cas limites.)

Un deuxième point est qu'au lieu d'exécuter le flux sur la carte d'entrée entrySet, je l'ai exécuté sur keySet. Cela rend le code un peu plus propre, je pense, au prix d'avoir à extraire des valeurs de la carte plutôt que de l'entrée de la carte. Par ailleurs, j'ai d'abord eu key -> keycomme premier argument toMap()et cela a échoué avec une erreur d'inférence de type pour une raison quelconque. Le changer en (X key) -> keyfonction, comme l'a fait Function.identity().

Une autre variante encore est la suivante:

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

Cela utilise à la Map.forEach()place des flux. C'est encore plus simple, je pense, car cela évite les collecteurs, qui sont un peu maladroits à utiliser avec les cartes. La raison en est que Map.forEach()donne la clé et la valeur en tant que paramètres séparés, alors que le flux n'a qu'une seule valeur - et vous devez choisir d'utiliser la clé ou l'entrée de carte comme valeur. Du côté négatif, cela n'a pas la richesse et la bonté des autres approches. :-)

Stuart Marks
la source
11
Function.identity() peut sembler cool, mais comme la première solution nécessite une recherche de carte / hachage pour chaque entrée, contrairement à toutes les autres solutions, je ne le recommanderais pas.
Holger
13

Une solution générique comme ça

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

Exemple

Map<String, String> input = new HashMap<String, String>();
input.put("string1", "42");
input.put("string2", "41");
Map<String, Integer> output = transform(input,
            (val) -> Integer.parseInt(val));
Sotirios Delimanolis
la source
Belle approche utilisant des génériques. Je pense que cela peut être amélioré un peu - voir ma réponse.
Stuart Marks du
13

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

Maps.transformValues(originalMap, val -> ...)
Alex Krauss
la source
J'aime cette approche, mais attention à ne pas lui passer une fonction java.util.Function. Puisqu'il attend com.google.common.base.Function, Eclipse donne une erreur inutile - il dit que Function n'est pas applicable pour Function, ce qui peut être déroutant: "La méthode transformValues ​​(Map <K, V1>, Function <? Super V1 , V2>) dans le type Maps ne s'applique pas aux arguments (Map <Foo, Bar>, Function <Bar, Baz>) "
mskfisher
Si vous devez réussir un java.util.Function, vous avez deux options. 1. Évitez le problème en utilisant un lambda pour permettre à l'inférence de type Java de le comprendre. 2. Utilisez une référence de méthode comme javaFunction :: apply pour produire un nouveau lambda que l'inférence de type peut comprendre.
Joe
5

Ma bibliothèque StreamEx qui améliore l'API de flux standard fournit une EntryStreamclasse qui convient mieux à la transformation de cartes:

Map<String, Integer> output = EntryStream.of(input).mapValues(Integer::valueOf).toMap();
Tagir Valeev
la source
4

Une alternative qui existe toujours à des fins d'apprentissage est de construire votre collecteur personnalisé via Collector.of () bien que le collecteur JDK toMap () ici soit succinct (+1 ici ).

Map<String,Integer> newMap = givenMap.
                entrySet().
                stream().collect(Collector.of
               ( ()-> new HashMap<String,Integer>(),
                       (mutableMap,entryItem)-> mutableMap.put(entryItem.getKey(),Integer.parseInt(entryItem.getValue())),
                       (map1,map2)->{ map1.putAll(map2); return map1;}
               ));
Ira
la source
J'ai commencé avec ce collecteur personnalisé comme base et je voulais ajouter qu'au moins lors de l'utilisation de parallelStream () au lieu de stream (), le binaryOperator devrait être réécrit en quelque chose de plus proche map2.entrySet().forEach(entry -> { if (map1.containsKey(entry.getKey())) { map1.get(entry.getKey()).merge(entry.getValue()); } else { map1.put(entry.getKey(),entry.getValue()); } }); return map1ou des valeurs seront perdues lors de la réduction.
user691154
3

Si cela ne vous dérange pas d'utiliser des bibliothèques tierces, ma bibliothèque 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, map agit sur les valeurs de la carte).

   MapX<String,Integer> 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<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
                               .bimap(this::newKey,Integer::parseInt);
John McClean
la source