Performance: HashSet em vez de ArrayList
Por Paulo Silveira em 04/10/06
Quando um programador começa com Java, ele rapidamente desiste das arrays para trabalhar com a ArrayList, que encapsula algumas rotinas comuns e trabalhosas. Depois o programador começa a se preocupar mais com o encapsulamento e passa a se refernciar as ArrayLists como List.
Um último passo que é mais díficil do programador tomar é de abstrair suas referências para Collection. Isso talvez ocorra por causa da óbvia ausência do método get(int) na interface Collection, apesar de que em muitos casos estamos apenas usando uma coleção para guardar um punhado de referências, sem nos importar com a ordem em que vamos percorrê-las e nem vamos precisar pegar uma referência que está em determinada posição (acesso aleatório). É nesse caso que a utilização dos Sets (em especial os que usam tabelas de espalhamento) é fortemente indicada (e mesmo que você precisasse garantir a ordem para iterar as referências, poderia usar uma coleção como a LinkedHashSet).
No curso de Java e Orientação a Objetos da Caelum, temos alguns exercícios extras. No capítulo de coleções tentamos mostrar a grande diferença de se usar um HashSet em vez de uma ArrayList quando precisamos realizar muitas buscas nessa coleção (isto é, usar o contains()). Você pode baixar essa apostila através do site.
Segue um código em que inserimos 30 mil referências a objetos do tipo Integer em uma ArrayList, e depois fazemos uma busca por cada um deles. Repare que 30 mil não é um número incomum.
public class TestaCollections {
public static void main(String[] args) {
Collection<Integer> colecao = new ArrayList<Integer>();
long tempoInicial = System.currentTimeMillis();
for (int i = 0; i < 30000; i++) {
colecao.add(i);
}
for (int i = 0; i < 30000; i++) {
colecao.contains(i);
}
long tempoFinal = System.currentTimeMillis();
System.out.printf("%.3f ms%n", (tempoFinal - tempoInicial) / 1000d);
}
}
O resultado: 8,891 ms.
Vamos agora realizar o mesmo teste só que com uma estrutura que utilize tabelas de espalhamento, nesse caso o HashSet. Mudamos a seguinte linha:
Collection<Integer> colecao = new ArrayList<Integer>();
Para:
Collection<Integer> colecao = new HashSet<Integer>();
O novo resultado? Incríveis 0,078 ms. 114 vezes mais rápido! E se você aumentar o tamanho dos laços dos testes, essa proporção só vai aumentar! Porque isso? O contains da ArrayList faz uma busca linear, já o HashSet utiliza uma tabela de espalhamento para tentar fazer a busca em tempo constante.
Você ainda pode fazer outros testes interessantes. Um TreeSet será mais devagar para inserir, pois mantém uma árvore rubro negra como estrutura de dados interna, e sua busca muito melhor que a da ArrayList e um pouco mais lenta que a do HashSet, porém teremos nossos elementos ordenados. Comparar a inserção e busca de elementos entre LinkedList e ArrayList também mostra números impressionantes: percorrer uma LinkedList sem usar um Iterator (ou enhanced for) é absurdamente lento, já que o seu método get(int) percorre a lista toda desde o começo, por sua vez, na ArrayList inserir elementos nas posições iniciais não é nada performático (é necessário deslocar todo o restante da array pra frente).
Obviamente uma boa função de hash também é essencial. Recentemente a Sun melhorou a função de hash dos nomes dos métodos que o compilador utilizava para depois procurá-los, e com apenas isso conseguiu uma melhoria de performance de 2% na execução geral. Imagine se eles utilizassem uma List para isso? Dominar as coleções e estruturas de dados do Java é fundamental para um bom código.
Nao que seja o caso aqui, mas eh sempre, seeeeeeempre importante lembrar que essas otimizacoes nao devem ser feitas antes de um bom estudo da performance da aplicacao, e nao adianta sair trocando ArrayList por HashSet a torto e a direito se o problema esta num LEFT OUTER JOIN monstruoso e sem indices.
Profiler. Sempre tenha um por perto
Comment by Carlos Villela — October 5, 2006 @ 5:48 am
Cara uma dúvida… porque vc percorreu um por um ??? com o método contains….
ele já faz isso pra vc… não precisa de um loop :S
correto?
Comment by Guilherme — October 6, 2006 @ 5:51 am
Não percorri um por um não, verifique o código: eu vejo um por um se o elemento I esta contido na coleção. apenas para testar a performance, ja que nao faco nada com o retorno booleano do metodo.
Comment by Paulo Silveira — October 6, 2006 @ 8:31 am
Eu nunca tenho meu profiler por perto. Por isso que semanalmente eu baixo o JProfiler e pego uma licensa nova dele…
Comment by Guilherme Silveira — October 6, 2006 @ 3:11 pm
relamente nao havia intendido… entendi sim a sua ideía de teste de perfomance… me desculpe pelo mal entendido.
no mais parabéns pelo artigo. muito interessante o HAshSet
Comment by Guilherme — October 6, 2006 @ 5:51 pm
o unico problema do hashset eh que ele usa um HashMap por traz, gerando varios Map.MapEntry’s (e um objeto como chave), enquanto o ArrayList usa um simples array.
Poderia se dizer que eh um compromisso entre processamento e consumo de memoria, certo?
Comment by takeshi — October 10, 2006 @ 9:10 am
Ola Takeshi. bom ponto. Creio que não faça tanta diferença, porque a array da arraylist tambem gasta bastante memória: 4 bytes para cada indice (tamanho da referencia considerando que a maquina virtual endereça 32 bits). O MapEntry pode acabar gasnta mais memoria, ja que guarda uma referencia a key e outra a value. Mas ambos os gastos ficarão muito pequeno em relação a memória total (deep) gasta por seus objetos pendurados nessas coleções.
Comment by Paulo Silveira — October 10, 2006 @ 9:32 am
Pô senhores, apesar de este assunto ainda ser para eu meu alienijena, ou seria alienigena, bem tanto faz, esta matéria estou vendo no curso. Parabéns pelo trabalho, digo comentário.
Um abraço.
Comment by Wagner — October 22, 2006 @ 1:12 pm
Paulo parabéns pelo artigo!
Gostaria de saber de vc, qual a melhor implementação de collection para usar com thread e a melhor forma de compartilhar uma única instancia dessa collection com várias threads.
Um abraço.
Comment by Nelson — April 1, 2008 @ 2:23 pm
Ola Nelson. Voce pode usar as collections de dentro do java.util.concurrent, que sao thread safe e algumas delas nem mesmo usam locks. Voce tambem pode usar as antigas Vector e Hashtable, mas eu nao aconselho, prefiro ate usar ArrayList e utilizar o Collections.synchronizedList junto com syncrhonized quando for iterar sob ela…
Comment by Paulo Silveira — April 1, 2008 @ 2:27 pm