operaciones - Java8: HashMap<X, Y> a HashMap<X, Z> usando Stream/Map-Reduce/Collector
stream java collect (9)
Sé cómo "transformar" una simple
List
Java de
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 ejemplo de la
List
anterior, me gustaría llamar a cualquier método (o constructor).
¿Tiene que ser absolutamente funcional y fluido al 100%? Si no, ¿qué tal esto, que es lo más corto 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 secuencias con efectos secundarios )
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 el flujo sobre el conjunto de entrada del mapa de
entrySet
, lo ejecuté sobre el conjunto de
keySet
.
Esto hace que el código sea un poco más limpio, creo, a costa de tener que extraer valores del mapa en lugar de hacerlo desde 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.
Cambiarlo a
(X key) -> key
funcionó, al igual que
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()
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.
:-)
La biblioteca My
StreamEx
que mejora la API de transmisión estándar proporciona una clase
EntryStream
que se adapta mejor para transformar mapas:
Map<String, Integer> output = EntryStream.of(input).mapValues(Integer::valueOf).toMap();
La función
Maps.transformValues
Guava es lo que está buscando, y funciona bien con expresiones lambda:
Maps.transformValues(originalMap, val -> ...)
La solución declarativa y más simple sería:
yourMutableMap.replaceAll ((key, val) -> return_value_of_bi_your_function); Nótese bien. tenga en cuenta que está modificando el estado de su mapa. Entonces esto puede no ser lo que quieres.
Si no le importa usar bibliotecas de terceros, mi cyclops-react tiene extensiones para todos los tipos de colección JDK , incluido Map . Simplemente podemos transformar el mapa directamente usando el operador ''mapa'' (de forma predeterminada, el mapa actúa sobre los valores del 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);
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 here ).
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;}
));
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));
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.Entry
s en una llamada
map()
por lo que el trabajo se mezcla con la llamada
collect()
.