Jon Skeet recientemente planteó un tema de programación interesante en su blog: "Hay un agujero en mi abstracción, querida Liza, querida Liza" (énfasis agregado):
Tengo un juego
HashSet, de hecho. Quiero eliminar algunos elementos ... y es posible que muchos de los elementos no existan. De hecho, en nuestro caso de prueba, ninguno de los elementos de la colección "eliminaciones" estará en el conjunto original. Esto suena, y de hecho es , extremadamente fácil de codificar. Después de todo, tenemos queSet<T>.removeAllayudarnos, ¿verdad?Especificamos el tamaño del conjunto "fuente" y el tamaño de la colección de "eliminaciones" en la línea de comando, y construimos ambos. El conjunto fuente contiene solo enteros no negativos; el conjunto de eliminaciones contiene solo números enteros negativos. Medimos cuánto tiempo lleva eliminar todos los elementos usando
System.currentTimeMillis(), que no es el cronómetro más preciso del mundo, pero es más que adecuado en este caso, como verás. Aquí está el código:import java.util.*; public class Test { public static void main(String[] args) { int sourceSize = Integer.parseInt(args[0]); int removalsSize = Integer.parseInt(args[1]); Set<Integer> source = new HashSet<Integer>(); Collection<Integer> removals = new ArrayList<Integer>(); for (int i = 0; i < sourceSize; i++) { source.add(i); } for (int i = 1; i <= removalsSize; i++) { removals.add(-i); } long start = System.currentTimeMillis(); source.removeAll(removals); long end = System.currentTimeMillis(); System.out.println("Time taken: " + (end - start) + "ms"); } }Comencemos por darle un trabajo fácil: un conjunto de fuentes de 100 elementos y 100 para eliminar:
c:UsersJonTest>java Test 100 100 Time taken: 1msBien, no esperábamos que fuera lento… claramente podemos acelerar un poco las cosas. ¿Qué tal una fuente de un millón de elementos y 300.000 elementos para eliminar?
c:UsersJonTest>java Test 1000000 300000 Time taken: 38msHmm. Eso todavía parece bastante rápido. Ahora siento que he sido un poco cruel, pidiéndole que haga todo eso. Hagámoslo un poco más fácil: 300.000 elementos de origen y 300.000 eliminaciones:
c:UsersJonTest>java Test 300000 300000 Time taken: 178131ms¿Perdóneme? ¿Casi tres minutos ? ¡Ay! Seguramente debería ser más fácil eliminar elementos de una colección más pequeña que la que manejamos en 38ms.
¿Alguien puede explicar por qué está sucediendo esto? ¿Por qué el HashSet<T>.removeAllmétodo es tan lento?
fuente

Respuestas:
El comportamiento está (algo) documentado en el javadoc :
Lo que esto significa en la práctica, cuando llamas
source.removeAll(removals);:si la
removalscolección es de un tamaño menor quesource, se llama alremovemétodo deHashSet, que es rápido.si la
removalscolección es de un tamaño igual o mayor que elsource, entoncesremovals.containsse llama, lo cual es lento para ArrayList.Arreglo rapido:
Tenga en cuenta que hay un error abierto que es muy similar a lo que describe. La conclusión parece ser que probablemente sea una mala elección, pero no se puede cambiar porque está documentado en el javadoc.
Como referencia, este es el código de
removeAll(en Java 8, no he comprobado otras versiones):fuente
ArrayList#containsera el culpable. Una mirada al código deAbstractSet#removeAlldio el resto de la respuesta.