Java8: HashMap <X, Y> a HashMap <X, Z> usando Stream / Map-Reduce / Collector

210

Sé cómo "transformar" un Java simple Listde Y-> Z, es decir:

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

Ahora me gustaría hacer básicamente lo mismo con un mapa, es decir:

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

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

La solución no debe limitarse a String-> Integer. Al igual que en el Listejemplo anterior, me gustaría llamar a cualquier método (o constructor).

Benjamin M
fuente

Respuestas:

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

No es tan bueno como el código de la lista. No puede construir nuevos Map.Entrycorreos electrónicos en una map()llamada, por lo que el trabajo se mezcla con la collect()llamada.

John Kugelman
fuente
59
Se puede reemplazar e -> e.getKey()con Map.Entry::getKey. Pero eso es una cuestión de gusto / estilo de programación.
Holger
55
En realidad, es una cuestión de rendimiento, su sugerencia es ser ligeramente superior al 'estilo' lambda
Jon Burgin
36

Aquí hay algunas variaciones en la respuesta de Sotirios Delimanolis , que fue bastante buena para empezar (+1). Considera lo siguiente:

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))));
}

Un par de puntos aquí. Primero está el uso de comodines en los genéricos; Esto hace que la función sea algo más flexible. Sería necesario un comodín si, por ejemplo, desea que el mapa de salida tenga una clave que sea una superclase de la clave del mapa de entrada:

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

(También hay un ejemplo para los valores del mapa, pero es realmente artificial, y admito que tener el comodín delimitado para Y solo ayuda en casos extremos).

Un segundo punto es que, en lugar de ejecutar la secuencia sobre el mapa de entrada entrySet, la ejecuté sobre keySet. Esto hace que el código sea un poco más limpio, creo, a costa de tener que recuperar valores del mapa en lugar de hacerlo desde la entrada del mapa. Por cierto, inicialmente tuve key -> keycomo primer argumento toMap()y esto falló con un error de inferencia de tipo por alguna razón. Cambiando a (X key) -> keytrabajado, como lo hizo Function.identity().

Todavía otra variación es la siguiente:

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;
}

Esto utiliza en Map.forEach()lugar de secuencias. Esto es aún más simple, creo, porque prescinde de los coleccionistas, que son algo torpes de usar con los mapas. La razón es que Map.forEach()proporciona la clave y el valor como parámetros separados, mientras que la secuencia solo tiene un valor, y debe elegir si usar la clave o la entrada del mapa como ese valor. En el lado negativo, esto carece de la bondad rica y fluida de los otros enfoques. :-)

Stuart Marks
fuente
11
Function.identity()puede parecer genial, pero dado que la primera solución requiere una búsqueda de mapa / hash para cada entrada, mientras que todas las demás soluciones no lo hacen, no lo recomendaría.
Holger
13

Una solución genérica como esta

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())));
}

Ejemplo

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
fuente
Buen enfoque utilizando genéricos. Sin embargo, creo que se puede mejorar un poco, vea mi respuesta.
Stuart Marks
13

La función de guayaba Maps.transformValueses lo que está buscando, y funciona muy bien con expresiones lambda:

Maps.transformValues(originalMap, val -> ...)
Alex Krauss
fuente
Me gusta este enfoque, pero tenga cuidado de no pasarle una función java.util. Como espera com.google.common.base.Function, Eclipse da un error inútil: dice que Function no es aplicable para Function, lo que puede ser confuso: "El método transformValues ​​(Map <K, V1>, Function <? Super V1 , V2>) en el tipo Mapas no es aplicable para los argumentos (Mapa <Foo, Bar>, Función <Bar, Baz>) "
mskfisher
Si debe pasar un java.util.Function, tiene dos opciones. 1. Evite el problema utilizando una lambda para permitir que la inferencia de tipo Java lo resuelva. 2. Utilice una referencia de método como javaFunction :: apply para producir una nueva lambda que pueda deducir la inferencia de tipos.
Joe
5

La biblioteca My StreamEx que mejora la API de flujo estándar proporciona una EntryStreamclase que se adapta mejor para transformar mapas:

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

Una alternativa que siempre existe con fines de aprendizaje es construir su recopilador personalizado a través de Collector.of (), aunque toMap () JDK collector aquí es sucinto (+1 aquí ).

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
fuente
Comencé con este recopilador personalizado como base y quería agregar que, al menos al usar parallelStream () en lugar de stream (), el binaryOperator debería reescribirse en algo más parecido map2.entrySet().forEach(entry -> { if (map1.containsKey(entry.getKey())) { map1.get(entry.getKey()).merge(entry.getValue()); } else { map1.put(entry.getKey(),entry.getValue()); } }); return map1o se perderán valores al reducirlo.
user691154
3

Si no le importa usar bibliotecas de terceros, mi libra cyclops-react tiene extensiones para todos los tipos de colección JDK , incluido Map . Simplemente podemos transformar el mapa directamente usando el operador 'mapa' (por defecto, el mapa actúa sobre los valores en el mapa).

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

bimap se puede usar para transformar las claves y los valores al mismo tiempo

  MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
                               .bimap(this::newKey,Integer::parseInt);
John McClean
fuente