java lambda java-8 java-stream

java - ¿Cómo eliminar varios elementos de Set/Map Y saber cuáles fueron eliminados?



lambda java-8 (6)

Tengo un método que tiene que eliminar cualquier elemento listado en un (pequeño) Set<K> keysToRemove para Set<K> keysToRemove de algún Map<K,V> from (potencialmente grande) Map<K,V> from . Pero removeAll() no funciona, ya que necesito devolver todas las claves que realmente se eliminaron, ya que el mapa puede o no contener claves que requieren ser eliminadas.

El código de la vieja escuela es sencillo:

public Set<K> removeEntries(Map<K, V> from) { Set<K> fromKeys = from.keySet(); Set<K> removedKeys = new HashSet<>(); for (K keyToRemove : keysToRemove) { if (fromKeys.contains(keyToRemove)) { fromKeys.remove(keyToRemove); removedKeys.add(keyToRemove); } } return removedKeys; }

Lo mismo, escrito usando streams:

Set<K> fromKeys = from.keySet(); return keysToRemove.stream() .filter(fromKeys::contains) .map(k -> { fromKeys.remove(k); return k; }) .collect(Collectors.toSet());

Lo encuentro un poco más conciso, pero también encuentro que la lambda es demasiado torpe.

¿Alguna sugerencia de cómo lograr el mismo resultado en formas menos torpes?


El "código de la vieja escuela" debería ser

public Set<K> removeEntries(Map<K, ?> from) { Set<K> fromKeys = from.keySet(), removedKeys = new HashSet<>(keysToRemove); removedKeys.retainAll(fromKeys); fromKeys.removeAll(removedKeys); return removedKeys; }

Ya que dijo que keysToRemove es bastante pequeño, es probable que la sobrecarga de copia no importe. De lo contrario, usa el bucle, pero no hagas la búsqueda de hash dos veces:

public Set<K> removeEntries(Map<K, ?> from) { Set<K> fromKeys = from.keySet(); Set<K> removedKeys = new HashSet<>(); for(K keyToRemove : keysToRemove) if(fromKeys.remove(keyToRemove)) removedKeys.add(keyToRemove); return removedKeys; }

Puedes expresar la misma lógica que una secuencia como

public Set<K> removeEntries(Map<K, ?> from) { return keysToRemove.stream() .filter(from.keySet()::remove) .collect(Collectors.toSet()); }

pero dado que este es un filtro de estado, es altamente desanimado. Una variante más limpia sería

public Set<K> removeEntries(Map<K, ?> from) { Set<K> result = keysToRemove.stream() .filter(from.keySet()::contains) .collect(Collectors.toSet()); from.keySet().removeAll(result); return result; }

y si desea maximizar el uso "continuo", puede reemplazar from.keySet().removeAll(result); con from.keySet().removeIf(result::contains) , que es from.keySet().removeIf(result::contains) caro, ya que está iterando sobre el mapa más grande, o con result.forEach(from.keySet()::remove) , que no tiene esa desventaja, pero aún así, no es más legible que removeAll .

Con todo, el "código de la vieja escuela" es mucho mejor que eso.


No usaría Streams para esto. Me aprovecharía de retainAll :

public Set<K> removeEntries(Map<K, V> from) { Set<K> matchingKeys = new HashSet<>(from.keySet()); matchingKeys.retainAll(keysToRemove); from.keySet().removeAll(matchingKeys); return matchingKeys; }


Para agregar otra variante a los enfoques, también se podrían dividir las claves y devolver el Set requerido como:

public Set<K> removeEntries(Map<K, ?> from) { Map<Boolean, Set<K>> partitioned = keysToRemove.stream() .collect(Collectors.partitioningBy(k -> from.keySet().remove(k), Collectors.toSet())); return partitioned.get(Boolean.TRUE); }


Puedes usar el stream y el removeAll

Set<K> fromKeys = from.keySet(); Set<K> removedKeys = keysToRemove.stream() .filter(fromKeys::contains) .collect(Collectors.toSet()); fromKeys.removeAll(removedKeys); return removedKeys;


Puedes usar esto:

Set<K> removedKeys = keysToRemove.stream() .filter(from::containsKey) .collect(Collectors.toSet()); removedKeys.forEach(from::remove);

Es similar a la respuesta de Oleksandr , pero evitando el efecto secundario. Pero me quedo con esa respuesta, si buscas rendimiento.

Alternativamente, puede usar Stream.peek() para eliminar, pero tenga cuidado con otros efectos secundarios (vea los comentarios). Así que no lo recomendaría.

Set<K> removedKeys = keysToRemove.stream() .filter(from::containsKey) .peek(from::remove) .collect(Collectors.toSet());


Solución más concisa, pero aún con efectos secundarios no deseados en la llamada de filter :

Set<K> removedKeys = keysToRemove.stream() .filter(fromKeys::remove) .collect(Collectors.toSet());

Set.remove ya devuelve true si el set contiene el elemento especificado.

PS Al final, probablemente me quedaría con el "código de la vieja escuela".