HashMap vs Java TreeMap

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.