programacion - jtextfield java
Transformar y filtrar un mapa de Java con flujos (6)
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 una manera de evitar llamar a .entrySet().stream()
dos veces.
¿Hay alguna forma de realizar operaciones de transformación y filtro y llamar a .collect()
solo una vez al final?
Aquí está el código de AbacusUtil
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 AbacusUtil.
Otra forma de hacerlo es eliminar los valores que no desea del Map
transformado:
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 desea un Map
mutable como resultado, si no, probablemente pueda crear uno inmutable a partir de la 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 que la input
es mutable.
No recomiendo hacer esto porque no funcionará para todas las implementaciones válidas de Map
y puede dejar de trabajar para HashMap
en el futuro, pero actualmente puede usar 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 proporcionará advertencias de seguridad de tipo y si intenta recuperar un valor del Map
través de una referencia del tipo anterior como esta:
String ex = input.get("a");
Se lanzará una ClassCastException
.
Podría mover la primera parte de transformación a un método para evitar la repetición si espera usarla 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 úsalo 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 Map
old
es un IdentityHashMap
y el mapFactory
crea un HashMap
.
Podría usar el Stream.collect(supplier, accumulator, combiner)
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 métodos Optional
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 está recolectando.
Sí, puede asignar cada entrada a otra entrada temporal que contendrá la clave y el valor entero analizado. A continuación, puede filtrar cada entrada en función de 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 utilicé Integer.valueOf
lugar de parseInt
ya que en realidad queremos un int
caja.
Si tiene el lujo de usar la biblioteca de StreamEx , puede hacerlo de manera bastante simple:
Map<String, Integer> output =
EntryStream.of(input).mapValues(Integer::valueOf).filterValues(v -> v % 2 == 0).toMap();
Una forma de resolver el problema con una sobrecarga mucho menor es mover la asignación y el filtrado hacia abajo 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 instancias de Map.Entry
intermedias y, aún mejor, pospondrá el recuadro de valores int
al punto en que los valores se agregan realmente al Map
, lo que implica que los valores rechazados por el filtro no están recuadrados en absoluto.
En comparación con lo que hace Collectors.toMap(…)
, la operación también se simplifica utilizando Map.put
lugar de Map.merge
ya que sabemos que no tenemos que manejar las colisiones de claves aquí.
Sin embargo, siempre que no desee utilizar la ejecución paralela, también puede considerar el bucle 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); });
este último es bastante compacto y al menos a la par con todas las otras variantes con respecto al rendimiento de un solo hilo.
Guava es tu amiga
Map<String, Integer> output = Maps.filterValues(Maps.transformValues(input, Integer::valueOf), i -> i % 2 == 0);
Tenga en cuenta que la output
es una vista de input
transformada y filtrada. Tendrá que hacer una copia si desea utilizarlos de forma independiente.