Pablo I
Tengo un mapa de Java que me gustaría transformar y filtrar. Como ejemplo trivial, supongamos que quiero convertir todos los valores a enteros y luego eliminar las entradas impares.
Map<String, String> input = new HashMap<>();
input.put("a", "1234");
input.put("b", "2345");
input.put("c", "3456");
input.put("d", "4567");
Map<String, Integer> output = input.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> Integer.parseInt(e.getValue())
))
.entrySet().stream()
.filter(e -> e.getValue() % 2 == 0)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
System.out.println(output.toString());
Esto es correcto y produce: {a=1234, c=3456}
Sin embargo, no puedo evitar preguntarme si hay alguna manera de evitar llamar .entrySet().stream()
dos veces.
¿Hay alguna forma en que pueda realizar operaciones de transformación y filtrado y llamar .collect()
solo una vez al final?
Tunaki
Sí, puede asignar cada entrada a otra entrada temporal que contendrá la clave y el valor entero analizado. Luego puede filtrar cada entrada según su valor.
Map<String, Integer> output =
input.entrySet()
.stream()
.map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), Integer.valueOf(e.getValue())))
.filter(e -> e.getValue() % 2 == 0)
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue
));
Tenga en cuenta que usé Integer.valueOf
en lugar de parseInt
ya que en realidad queremos una caja int
.
Si tienes el lujo de usar el StreamEx biblioteca, puede hacerlo de manera bastante simple:
Map<String, Integer> output =
EntryStream.of(input).mapValues(Integer::valueOf).filterValues(v -> v % 2 == 0).toMap();
-
Si bien esta es una forma posible de hacerlo, siento que estamos sacrificando el rendimiento y la legibilidad por unas pocas líneas de código, ¿no es así?
– kosa
18/02/2016 a las 16:30
-
@Nambari No veo por qué es tan “incorrecto”. Es solo un filtro de mapa. Si es el uso explícito de
AbstractMap.SimpleEntry
puedes crear otroPair
pero siento que es apropiado aquí ya que estamos tratando con mapas.– Tunaki
18 de febrero de 2016 a las 16:32
-
Usé “hacky” solo por la “entrada temporal” que estamos rastreando, puede que no sea el término correcto. Me gustó la solución StreamEx.
– kosa
18 de febrero de 2016 a las 16:34
-
@Nambari, tenga en cuenta que StreamEx hace lo mismo internamente, es solo un azúcar sintáctico.
– Tagir Valéev
19 de febrero de 2016 a las 6:20
-
@TagirValeev: Sí, lo sé. Cuando la API no es compatible, todas estas bibliotecas solo hacen el azúcar sintáctico.
– kosa
19 de febrero de 2016 a las 14:22
Una forma de resolver el problema con una sobrecarga mucho menor es mover el mapeo y el filtrado al colector.
Map<String, Integer> output = input.entrySet().stream().collect(
HashMap::new,
(map,e)->{ int i=Integer.parseInt(e.getValue()); if(i%2==0) map.put(e.getKey(), i); },
Map::putAll);
Esto no requiere la creación de intermediarios. Map.Entry
instancias y mejor aún, pospondrá el boxeo de int
valores hasta el punto en que los valores se suman realmente a la Map
lo que implica que los valores rechazados por el filtro no se encuadran en absoluto.
Comparado con que Collectors.toMap(…)
hace, la operación también se simplifica usando Map.put
en vez de Map.merge
como sabemos de antemano que no tenemos que manejar colisiones de teclas aquí.
Sin embargo, siempre que no desee utilizar la ejecución paralela, también puede considerar el ciclo ordinario
HashMap<String,Integer> output=new HashMap<>();
for(Map.Entry<String, String> e: input.entrySet()) {
int i = Integer.parseInt(e.getValue());
if(i%2==0) output.put(e.getKey(), i);
}
o la variante de iteración interna:
HashMap<String,Integer> output=new HashMap<>();
input.forEach((k,v)->{ int i = Integer.parseInt(v); if(i%2==0) output.put(k, i); });
siendo este último bastante compacto y al menos a la par con todas las demás variantes con respecto al rendimiento de un solo subproceso.
-
Voté a favor de su recomendación de un bucle simple. El hecho de que pueda usar secuencias no significa que deba hacerlo.
–Jeffrey Bosboom
18 de febrero de 2016 a las 20:04
-
@Jeffrey Bosboom: sí, el buen viejo
for
el bucle sigue vivo. Aunque en el caso de los mapas, me gusta usar elMap.forEach
método para bucles más pequeños solo porque(k,v)->
es mucho mejor que declarar unMap.Entry<PossiblyLongKeyType,PossiblyLongValueType>
variable y posiblemente otras dos variables para la clave y el valor reales…– Holger
18 de febrero de 2016 a las 20:09
smosel
Guayabaes tu amigo:
Map<String, Integer> output = Maps.filterValues(Maps.transformValues(input, Integer::valueOf), i -> i % 2 == 0);
Manten eso en mente output
es un transformado, filtrado vista de input
. Deberá hacer una copia si desea operar con ellos de forma independiente.
fps
Podrías usar el Stream.collect(supplier, accumulator, combiner)
método para transformar las entradas y acumularlas condicionalmente:
Map<String, Integer> even = input.entrySet().stream().collect(
HashMap::new,
(m, e) -> Optional.ofNullable(e)
.map(Map.Entry::getValue)
.map(Integer::valueOf)
.filter(i -> i % 2 == 0)
.ifPresent(i -> m.put(e.getKey(), i)),
Map::putAll);
System.out.println(even); // {a=1234, c=3456}
Aquí, dentro del acumulador, estoy usando Optional
métodos para aplicar tanto la transformación como el predicado y, si el valor opcional todavía está presente, lo estoy agregando al mapa que se recopila.
Alex – GlassEditor.com
Otra forma de hacer esto es eliminar los valores que no desea del transformado Map
:
Map<String, Integer> output = input.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> Integer.parseInt(e.getValue()),
(a, b) -> { throw new AssertionError(); },
HashMap::new
));
output.values().removeIf(v -> v % 2 != 0);
Esto supone que quieres un mutable Map
como resultado, si no, probablemente puedas crear uno inmutable a partir de output
.
Si está transformando los valores en el mismo tipo y desea modificar el Map
en su lugar, esto podría ser mucho más corto con replaceAll
:
input.replaceAll((k, v) -> v + " example");
input.values().removeIf(v -> v.length() > 10);
Esto también supone input
es mutable.
No recomiendo hacer esto porque no funcionará para todos los válidos. Map
implementaciones y puede dejar de funcionar para HashMap
en el futuro, pero actualmente puede utilizar replaceAll
y lanzar un HashMap
para cambiar el tipo de los valores:
((Map)input).replaceAll((k, v) -> Integer.parseInt((String)v));
Map<String, Integer> output = (Map)input;
output.values().removeIf(v -> v % 2 != 0);
Esto también le dará advertencias de seguridad y si intenta recuperar un valor del Map
a través de una referencia del tipo antiguo como esta:
String ex = input.get("a");
lanzará un ClassCastException
.
Puede mover la primera parte de transformación a un método para evitar el repetitivo si espera usarlo mucho:
public static <K, VO, VN, M extends Map<K, VN>> M transformValues(
Map<? extends K, ? extends VO> old,
Function<? super VO, ? extends VN> f,
Supplier<? extends M> mapFactory){
return old.entrySet().stream().collect(Collectors.toMap(
Entry::getKey,
e -> f.apply(e.getValue()),
(a, b) -> { throw new IllegalStateException("Duplicate keys for values " + a + " " + b); },
mapFactory));
}
Y utilízalo así:
Map<String, Integer> output = transformValues(input, Integer::parseInt, HashMap::new);
output.values().removeIf(v -> v % 2 != 0);
Tenga en cuenta que la excepción de clave duplicada se puede lanzar si, por ejemplo, el old
Map
es un IdentityHashMap
y el mapFactory
crea un HashMap
.
-
Su
"Duplicate keys " + a + " " + b
El mensaje es engañoso:a
yb
son en realidad valores, no claves.– Tagir Valéev
19 de febrero de 2016 a las 6:22
-
@TagirValeev sí, lo noté, pero es de la misma manera que se hace en
Collectors
para la versión de dos argumentos detoMap
y cambiarlo a lo que había considerado pondría una barra de desplazamiento en el cuadro de código, así que decidí dejarlo. Lo cambiaré ahora ya que también crees que es engañoso.– Alex – GlassEditor.com
19 de febrero de 2016 a las 6:28
-
Sí, este es un problema conocido que ya está solucionado en Java-9 (pero no adaptado a Java-8). Java-9 muestra la clave de colisión, así como ambos valores en el mensaje de excepción.
– Tagir Valéev
19 de febrero de 2016 a las 6:43
-
Lo divertido de tu truco inseguro es que el
groupingBy
colector hace algo similar detrás de escena que puede ser una gran sorpresa cuando utiliza un proveedor que crea implementaciones de mapas que hacen cumplir la seguridad de tipo, por ejemplo()->Collections.checkedMap(new HashMap<>(), …)
– Holger
19 de febrero de 2016 a las 9:40
Aquí está el código de ábaco-común
Map<String, String> input = N.asMap("a", "1234", "b", "2345", "c", "3456", "d", "4567");
Map<String, Integer> output = Stream.of(input)
.groupBy(e -> e.getKey(), e -> N.asInt(e.getValue()))
.filter(e -> e.getValue() % 2 == 0)
.toMap(Map.Entry::getKey, Map.Entry::getValue);
N.println(output.toString());
Declaración: Soy el desarrollador de abacus-common.
-
Su
"Duplicate keys " + a + " " + b
El mensaje es engañoso:a
yb
son en realidad valores, no claves.– Tagir Valéev
19 de febrero de 2016 a las 6:22
-
@TagirValeev sí, lo noté, pero es de la misma manera que se hace en
Collectors
para la versión de dos argumentos detoMap
y cambiarlo a lo que había considerado pondría una barra de desplazamiento en el cuadro de código, así que decidí dejarlo. Lo cambiaré ahora ya que también crees que es engañoso.– Alex – GlassEditor.com
19 de febrero de 2016 a las 6:28
-
Sí, este es un problema conocido que ya está solucionado en Java-9 (pero no adaptado a Java-8). Java-9 muestra la clave de colisión, así como ambos valores en el mensaje de excepción.
– Tagir Valéev
19 de febrero de 2016 a las 6:43
-
Lo divertido de tu truco inseguro es que el
groupingBy
colector hace algo similar detrás de escena que puede ser una gran sorpresa cuando utiliza un proveedor que crea implementaciones de mapas que hacen cumplir la seguridad de tipo, por ejemplo()->Collections.checkedMap(new HashMap<>(), …)
– Holger
19 de febrero de 2016 a las 9:40
No creo que sea posible. Basado en javadoc “Se debe operar una transmisión (invocando una operación de transmisión intermedia o terminal) solo una vez. Esto descarta, por ejemplo, transmisiones “bifurcadas”, donde la misma fuente alimenta dos o más tuberías, o múltiples recorridos de la misma flujo. Una implementación de flujo puede lanzar IllegalStateException si detecta que el flujo se está reutilizando”.
– kosa
18 de febrero de 2016 a las 16:25
@Namban De eso no se trata la pregunta.
– usuario253751
18/02/2016 a las 20:00