Jon Skeet recentemente levantou um tópico de programação interessante em seu blog: "Há um buraco na minha abstração, querida Liza, querida Liza" (grifo nosso):
Eu tenho um conjunto - um
HashSet
, na verdade. Quero remover alguns itens dele ... e muitos dos itens podem não existir. Na verdade, em nosso caso de teste, nenhum dos itens na coleção de "remoções" estará no conjunto original. Isso parece - e de fato é - extremamente fácil de codificar. Afinal, temos queSet<T>.removeAll
nos ajudar, certo?Especificamos o tamanho do conjunto "fonte" e o tamanho da coleção "remoções" na linha de comando e construímos ambos. O conjunto de origem contém apenas inteiros não negativos; o conjunto de remoções contém apenas inteiros negativos. Medimos quanto tempo leva para remover todos os elementos usando
System.currentTimeMillis()
, o que não é o cronômetro mais preciso do mundo, mas é mais do que adequado neste caso, como você verá. Aqui está o 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"); } }
Vamos começar dando a ele um trabalho fácil: um conjunto de origem de 100 itens e 100 para remover:
c:UsersJonTest>java Test 100 100 Time taken: 1ms
Ok, então não esperávamos que fosse lento ... claramente podemos acelerar um pouco as coisas. Que tal uma fonte de um milhão de itens e 300.000 itens para remover?
c:UsersJonTest>java Test 1000000 300000 Time taken: 38ms
Hmm. Isso ainda parece muito rápido. Agora sinto que fui um pouco cruel, pedindo-lhe para fazer toda aquela remoção. Vamos tornar um pouco mais fácil - 300.000 itens de origem e 300.000 remoções:
c:UsersJonTest>java Test 300000 300000 Time taken: 178131ms
Desculpe? Quase três minutos ? Caramba! Certamente deveria ser mais fácil remover itens de uma coleção menor do que aquela que gerenciamos em 38ms?
Alguém pode explicar por que isso está acontecendo? Por que o HashSet<T>.removeAll
método é tão lento?
fonte
Respostas:
O comportamento é (de certa forma) documentado no javadoc :
O que isso significa na prática, quando você liga
source.removeAll(removals);
:se a
removals
coleção for de um tamanho menor quesource
, oremove
método deHashSet
é chamado, que é rápido.se a
removals
coleção for de tamanho igual ou maior que osource
, entãoremovals.contains
é chamado, o que é lento para um ArrayList.Conserto rápido:
Observe que há um bug aberto que é muito semelhante ao que você descreve. O resultado final parece ser que provavelmente é uma escolha ruim, mas não pode ser alterada porque está documentada no javadoc.
Para referência, este é o código de
removeAll
(em Java 8 - não verifiquei outras versões):fonte
ArrayList#contains
era o culpado. Uma olhada no código deAbstractSet#removeAll
deu o resto da resposta.