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>.removeAll
ayudarnos, ¿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: 1ms
Bien, 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: 38ms
Hmm. 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>.removeAll
mé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
removals
colección es de un tamaño menor quesource
, se llama alremove
método deHashSet
, que es rápido.si la
removals
colección es de un tamaño igual o mayor que elsource
, entoncesremovals.contains
se 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#contains
era el culpable. Una mirada al código deAbstractSet#removeAll
dio el resto de la respuesta.