HashMap vs Java TreeMap
*1. Introdução *
Neste artigo, vamos comparar duas implementações Map: TreeMap e HashMap.
Ambas as implementações fazem parte integrante do Java Collections Framework e armazenam dados como pares key-value.
===* 2. Diferenças *
====* 2.1 Implementação *
Primeiro falaremos sobre o HashMap, que é uma implementação baseada em hashtable. Ele estende a classe AbstractMap e implementa a interface Map. Um HashMap trabalha com o princípio de _link:/java-hashcode [hashing] _.
Essa implementação Map geralmente atua como uma hash table em bloco, mas* quando os buckets ficam muito grandes, eles são transformados em nós de TreeNodes *, cada um estruturado de maneira semelhante aos de java.util.TreeMap.
Você pode encontrar mais informações sobre os HashMap’s internals no link:/java-hashmap [o artigo focado nele].
Por outro lado, TreeMap estende a classe AbstractMap e implementa a interface NavigableMap. Um TreeMap armazena elementos de mapa em uma árvore Red-Black, que é uma Árvore de Pesquisa Binária com Auto Balanceamento_ .
E também é possível encontrar mais informações sobre os TreeMap’s internals no link:/java-treemap [o artigo focado aqui].
* 2.2 Ordem*
*_HashMap_ não fornece nenhuma garantia sobre a maneira como os elementos são organizados no _Map_* .
Significa não podemos assumir nenhuma ordem enquanto iteramos sobre keys e values de um _HashMap _:
@Test
public void whenInsertObjectsHashMap_thenRandomOrder() {
Map<Integer, String> hashmap = new HashMap<>();
hashmap.put(3, "TreeMap");
hashmap.put(2, "vs");
hashmap.put(1, "HashMap");
assertThat(hashmap.keySet(), containsInAnyOrder(1, 2, 3));
}
No entanto, os itens em um TreeMap são classificados de acordo com sua ordem natural .
Se os objetos TreeMap não puderem ser classificados de acordo com a ordem natural, poderemos usar um Comparator ou Comparable para definir a ordem em que os elementos são organizados no _Map: _
@Test
public void whenInsertObjectsTreeMap_thenNaturalOrder() {
Map<Integer, String> treemap = new TreeMap<>();
treemap.put(3, "TreeMap");
treemap.put(2, "vs");
treemap.put(1, "HashMap");
assertThat(treemap.keySet(), contains(1, 2, 3));
}
2.3 Null Valores
HashMap permite armazenar no máximo um null key e muitos valores null.
Vamos ver um exemplo:
@Test
public void whenInsertNullInHashMap_thenInsertsNull() {
Map<Integer, String> hashmap = new HashMap<>();
hashmap.put(null, null);
assertNull(hashmap.get(null));
}
No entanto, TreeMap não permite um null key, mas pode conter muitos valores null.
Uma chave null não é permitida porque o método _compareTo () _ ou _compare () _ lança uma _NullPointerException: _
@Test(expected = NullPointerException.class)
public void whenInsertNullInTreeMap_thenException() {
Map<Integer, String> treemap = new TreeMap<>();
treemap.put(null, "NullPointerException");
}
*Se estivermos usando um _TreeMap_ com um _Comparator_ definido pelo usuário, isso dependerá da implementação do método compare __ () __ como os valores _null_ são tratados.
===* 3. Análise de desempenho *
O desempenho é a métrica mais crítica que nos ajuda a entender a adequação de uma estrutura de dados, considerando um caso de uso.
Nesta seção, forneceremos uma análise abrangente do desempenho para HashMap e TreeMap.
====* 3.1 HashMap *
*_HashMap, _ sendo uma implementação baseada em hashtable, usa internamente uma estrutura de dados baseada em array para organizar seus elementos de acordo com a função _hash _.*
HashMap fornece desempenho esperado em tempo constante O (1) _ para a maioria das operações como _add () _, _remove () _ e _contains () . Portanto, é significativamente mais rápido que um TreeMap.
O tempo médio para procurar um elemento sob a suposição razoável, em uma tabela de hash é O (1) . Porém, uma implementação incorreta da função hash pode levar a uma má distribuição de valores nos buckets, o que resulta em:
-
Sobrecarga de memória - muitos buckets permanecem sem uso Degradação do desempenho - * quanto maior o número de colisões, menor o desempenho
*Antes do Java 8, _Separate Chaining_ era a única maneira preferida de lidar com colisões.* Geralmente é implementado usando listas vinculadas, ie, se houver alguma colisão ou se dois elementos diferentes tiverem o mesmo valor de hash, armazene os dois itens no mesma lista vinculada.
Portanto, procurar um elemento em um HashMap, _ no pior caso, poderia levar tanto tempo quanto procurar um elemento em uma lista vinculada _i.e. _O (n) _ tempo.
*No entanto, com https://openjdk.java.net/jeps/180[JEP 180] entrando em cena, houve uma mudança sutil na implementação da maneira como os elementos são organizados em um* *_HashMap ._*
De acordo com a especificação, quando os buckets ficam muito grandes e contêm nós suficientes, eles são transformados em modos de TreeNodes, cada um estruturado de maneira semelhante aos do TreeMap.
*Portanto, no caso de colisões de hash altas, o desempenho do pior caso será aprimorado de _O (n) _ para _O (log n) ._*
O código que executa essa transformação foi ilustrado abaixo:
if(binCount >= TREEIFY_THRESHOLD - 1) {
treeifyBin(tab, hash);
}
O valor para TREEIFY_THRESHOLD é oito, o que denota efetivamente a contagem de limites para o uso de uma árvore em vez de uma lista vinculada para um bucket.
É evidente que:
-
Um HashMap requer muito mais memória do que o necessário para armazenar seus dados
-
Um HashMap não deve ter mais que 70% - 75% da capacidade total. Se se aproximar, será redimensionado e as entradas repetidas
-
Rehashing requer n operações que são caras, em que nossa inserção de tempo constante se torna da ordem _O (n) _
-
É o algoritmo de hash que determina a ordem de inserção dos objetos no HashMap
*O desempenho de um _HashMap_ pode ser ajustado, definindo a capacidade inicial _ personalizada e o fator de carga_* , no momento da criação do objeto _HashMap_.
No entanto, devemos escolher um HashMap se:
-
sabemos aproximadamente quantos itens manter em nossa coleção *não queremos extrair itens em uma ordem natural
Nas circunstâncias acima, HashMap é a nossa melhor escolha, pois oferece inserção, pesquisa e exclusão de tempo constante.
====* 3.2 TreeMap *
Um TreeMap armazena seus dados em uma árvore hierárquica com a capacidade de classificar os elementos com a ajuda de um Comparator. personalizado.
Um resumo de seu desempenho:
-
TreeMap fornece um desempenho de _O (log (n)) _ para a maioria das operações como _add () _, _remove () _ e _contains () _
-
Um Treemap pode economizar memória (em comparação com HashMap) _ porque ele usa apenas a quantidade de memória necessária para armazenar seus itens, ao contrário de um _HashMap que usa região contígua da memória
-
Uma árvore deve manter seu equilíbrio para manter o desempenho pretendido, isso requer uma quantidade considerável de esforço e, portanto, complica a implementação
Deveríamos procurar um TreeMap sempre que:
-
limitações de memória devem ser levadas em consideração
-
não sabemos quantos itens precisam ser armazenados na memória
-
queremos extrair objetos em uma ordem natural
-
se os itens serão constantemente adicionados e removidos *estamos dispostos a aceitar _O (log n) _ tempo de pesquisa
===* 4. Semelhanças *
====* 4.1 Elementos exclusivos *
TreeMap e HashMap não suportam chaves duplicadas. Se adicionado, substitui o elemento anterior (sem erro ou exceção):
@Test
public void givenHashMapAndTreeMap_whenputDuplicates_thenOnlyUnique() {
Map<Integer, String> treeMap = new HashMap<>();
treeMap.put(1, "Baeldung");
treeMap.put(1, "Baeldung");
assertTrue(treeMap.size() == 1);
Map<Integer, String> treeMap2 = new TreeMap<>();
treeMap2.put(1, "Baeldung");
treeMap2.put(1, "Baeldung");
assertTrue(treeMap2.size() == 1);
}
====* 4.2 Acesso simultâneo *
*Ambas as implementações de _Map_ não são _sincronizadas_* e precisamos gerenciar o acesso simultâneo por conta própria.
Ambos devem ser sincronizados externamente sempre que vários threads os acessam simultaneamente e pelo menos um deles os modifica.
Temos que usar explicitamente _Collections.synchronizedMap (mapName) _ para obter uma exibição sincronizada de um mapa fornecido.
4.3 Iteradores à prova de falhas
O Iterator lança uma ConcurrentModificationException se o Map for modificado de qualquer maneira e a qualquer momento após a criação do iterador.
Além disso, podemos usar o método de remoção do iterador para alterar o Map durante a iteração.
Vamos ver um exemplo:
@Test
public void whenModifyMapDuringIteration_thenThrowExecption() {
Map<Integer, String> hashmap = new HashMap<>();
hashmap.put(1, "One");
hashmap.put(2, "Two");
Executable executable = () -> hashmap
.forEach((key,value) -> hashmap.remove(1));
assertThrows(ConcurrentModificationException.class, executable);
}
*5. Qual implementação usar? *
Em geral, ambas as implementações têm seus respectivos prós e contras, no entanto,* trata-se de entender as expectativas e os requisitos subjacentes que devem governar nossa escolha em relação à mesma. *
Resumindo:
-
Devemos usar um TreeMap se quisermos manter nossas entradas classificadas
-
Devemos usar um HashMap se priorizarmos o desempenho sobre o consumo de memória
-
Como um TreeMap tem uma localidade mais significativa, podemos considerá-lo se quisermos acessar objetos que são relativamente próximos um do outro, de acordo com sua ordem natural
-
O HashMap pode ser ajustado usando o initialCapacity e o loadFactor, o que não é possível para o TreeMap *Podemos usar o LinkedHashMap se quisermos preservar o pedido de inserção enquanto nos beneficiamos do acesso em tempo constante
===* 6. Conclusão*
Neste artigo, mostramos as diferenças e semelhanças entre TreeMap e HashMap.
Como sempre, os exemplos de código deste artigo estão disponíveis over no GitHub.