Java8: mapa hash a HashMap usando Stream / Map-Reduce / Collector

5 minutos de lectura

avatar de usuario
benjamin m

Sé cómo “transformar” un Java simple List de Y -> Zes 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 List ejemplo anterior, me gustaría llamar a cualquier método (o constructor).

avatar de usuario
Juan Kugelman

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 agradable como el código de la lista. No puedes construir nuevos Map.Entryestá en un map() llamar para que el trabajo se mezcle con el collect() llamar.

  • Puedes reemplazar e -> e.getKey() con Map.Entry::getKey. Pero eso es cuestión de gustos/estilo de programación.

    – Holger

    18 de septiembre de 2014 a las 9:49

  • En realidad, es una cuestión de rendimiento, sugiriendo ser ligeramente superior al ‘estilo’ lambda

    – Jon Burgin

    20/04/2017 a las 20:35

avatar de usuario
marcas de estuardo

Aquí hay algunas variaciones de 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 acotado para Y solo ayuda en los casos extremos).

Un segundo punto es que en lugar de ejecutar la secuencia sobre el mapa de entrada entrySetlo pasé por encima keySet. Creo que esto hace que el código sea un poco más limpio, a costa de tener que obtener valores del mapa en lugar de la entrada del mapa. Por cierto, inicialmente tenía key -> key como primer argumento para toMap() y esto falló con un error de inferencia de tipo por alguna razón. cambiándolo a (X key) -> key trabajado, 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 usa Map.forEach() en lugar de arroyos. Esto es aún más simple, creo, porque prescinde de los colectores, que son algo torpes para usar con mapas. La razón es que Map.forEach() da la clave y el valor como parámetros separados, mientras que la transmisión 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. 🙂

  • Function.identity() puede verse bien, 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 la recomendaría.

    – Holger

    18 de septiembre de 2014 a las 9:53

Una solución genérica como tal

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

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

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

avatar de usuario
lukas eder

¿Tiene que ser absolutamente 100% funcional y fluido? Si no, ¿qué tal esto, que es lo más breve posible?

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

(si puedes vivir con la vergüenza y la culpa de combinar streams con efectos secundarios)

avatar de usuario
Tagir Valéev

Mi StreamEx biblioteca que mejora la API de flujo estándar proporciona una EntryStream clase que se adapta mejor para transformar mapas:

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

avatar de usuario
Comunidad

Una alternativa que siempre existe con fines de aprendizaje es crear su recopilador personalizado a través de Collector.of() aunque toMap() JDK recopilador 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;}
               ));

  • Comencé con este recopilador personalizado como base y quería agregar eso, al menos cuando se usa paraleloStream() en lugar de stream(), el operador binario debe reescribirse en algo más parecido a map2.entrySet().forEach(entry -> { if (map1.containsKey(entry.getKey())) { map1.get(entry.getKey()).merge(entry.getValue()); } else { map1.put(entry.getKey(),entry.getValue()); } }); return map1 o se perderán valores al reducir.

    – usuario691154

    14 de diciembre de 2016 a las 9:41


¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad