studio programacion ejemplo color java java-8 java-stream collectors

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.