Java TreeMap vs HashMap

1. Introduction

Dans cet article, nous allons comparer deux implémentations de Map :

TreeMap et HashMap .

Les deux implémentations font partie intégrante de Java Collections Framework et stockent les données sous forme de paires key-value .

2. Différences

2.1. La mise en oeuvre

Nous allons d’abord parler de HashMap , qui est une implémentation basée sur une table de hachage. Il étend la classe AbstractMap et implémente l’interface Map . Un HashMap fonctionne sur le principe de hashing .

Cette implémentation de Map agit généralement comme une table de hachage bâchée, mais lorsque les compartiments deviennent trop grands, ils sont transformés en nœuds de TreeNodes , chacun structuré de la même manière que ceux de java.util.TreeMap.__

Vous pouvez en savoir plus sur les éléments internes de __HashMap dans le lien:/java-hashmap[l’article qui s’y est concentré].

D’autre part, TreeMap étend la classe AbstractMap et implémente l’interface NavigableMap . Un TreeMap stocke les éléments de la carte dans un arbre Red-Black , qui est un Arbre de recherche binaire auto-équilibré__ .

Et, vous pouvez également trouver plus d’informations sur les éléments internes de __TreeMap dans le lien:/java-treemap[l’article est consacré à cela ici].

2.2. Ordre

  • HashMap ne fournit aucune garantie sur la façon dont les éléments sont disposés dans le Map ** .

Cela signifie, nous ne pouvons assumer aucun ordre lors de l’itération sur keys et values d’un 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));
}

Cependant, les éléments d’un TreeMap sont triés selon leur ordre naturel .

Si les objets TreeMap ne peuvent pas être triés selon l’ordre naturel, nous pouvons utiliser un Comparator ou Comparable pour définir l’ordre dans lequel les éléments sont organisés dans la 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 Values ​​

HashMap permet de stocker au plus une null key et plusieurs null valeurs.

Voyons un exemple:

@Test
public void whenInsertNullInHashMap__thenInsertsNull() {
    Map<Integer, String> hashmap = new HashMap<>();
    hashmap.put(null, null);

    assertNull(hashmap.get(null));
}

Cependant, TreeMap n’autorise pas null key mais peut contenir de nombreuses valeurs null .

Une clé null n’est pas autorisée car la méthode compareTo () ou compare () lève une exception NullPointerException:

@Test(expected = NullPointerException.class)
public void whenInsertNullInTreeMap__thenException() {
    Map<Integer, String> treemap = new TreeMap<>();
    treemap.put(null, "NullPointerException");
}
  • Si nous utilisons un TreeMap avec un Comparator défini par l’utilisateur, cela dépend de l’implémentation de la méthode compare _ () comment les valeurs null_ sont traitées.

3. Analyse de performance

La performance est la métrique la plus critique qui nous aide à comprendre l’adéquation d’une structure de données à un cas d’utilisation.

Dans cette section, nous fournirons une analyse complète des performances pour HashMap et TreeMap.

3.1. HashMap

  • HashMap, étant une implémentation basée sur une table de hachage, utilise en interne une structure de données basée sur un tableau pour organiser ses éléments en fonction de la fonction hash . **

HashMap fournit les performances attendues en temps constant O (1) pour la plupart des opérations telles que add () , remove () et contains () . Par conséquent, il est nettement plus rapide qu’un TreeMap .

Le temps moyen nécessaire pour rechercher un élément sous l’hypothèse raisonnable dans une table de hachage est de O (1) . Cependant, une implémentation incorrecte de la fonction__hash peut entraîner une mauvaise répartition des valeurs dans des compartiments, ce qui entraîne:

  • Surcharge mémoire - de nombreux compartiments restent inutilisés

  • Performance Degradation - Plus le nombre de collisions est élevé, plus le

abaisser la performance

  • Avant Java 8, Separate Chaining était le seul moyen privilégié de gérer les collisions. ** Il est généralement implémenté à l’aide de listes chaînées, i.e. , s’il y a une collision ou si deux éléments différents ont la même valeur de hachage, puis stockez les deux éléments dans le dossier. même liste chaînée.

Par conséquent, la recherche d’un élément dans un HashMap, dans le pire des cas, aurait pu prendre aussi longtemps que la recherche d’un élément dans une liste liée i.e. O (n) temps.

  • Cependant, avec JEP 180 à venir, il y a eu un changement subtil dans la mise en œuvre de la façon dont les éléments sont organisés dans un __ HashMap.

Selon la spécification, lorsque les compartiments deviennent trop grands et contiennent suffisamment de nœuds, ils sont transformés en modes de TreeNodes , chacun structuré de la même manière que ceux de TreeMap .

  • Par conséquent, en cas de collisions de hachage élevées, les performances dans le cas le plus défavorable s’amélioreront de O (n) à O (log n) . **

Le code effectuant cette transformation est illustré ci-dessous:

if(binCount >= TREEIFY__THRESHOLD - 1) {
    treeifyBin(tab, hash);
}

La valeur de TREEIFY THRESHOLD__ est huit, ce qui correspond effectivement au nombre de seuils pour l’utilisation d’un arbre plutôt qu’à une liste liée pour un compartiment.

C’est évident que:

  • Un HashMap nécessite beaucoup plus de mémoire que nécessaire pour contenir ses données

  • Un HashMap ne devrait pas être plein à plus de 70% - 75%. Si ça se rapproche,

il est redimensionné et les entrées ressaisies ** Le rehachage nécessite des opérations n qui sont coûteuses dans lesquelles notre

le temps inséré devient d’ordre O (n) ** C’est l’algorithme de hachage qui détermine l’ordre dans lequel insérer le

objets dans le HashMap

  • Les performances d’un HashMap peuvent être ajustées en définissant le initial capacité personnalisé et le load factor ** , au moment de la création de l’objet HashMap .

Cependant, nous devrions choisir un HashMap si:

  • nous savons environ combien d’articles à conserver dans notre collection

  • nous ne voulons pas extraire les objets dans un ordre naturel

Dans les circonstances ci-dessus, HashMap est notre meilleur choix car il offre une insertion, une recherche et une suppression en temps constant.

3.2. TreeMap

Un TreeMap stocke ses données dans une arborescence hiérarchique avec la possibilité de trier les éléments à l’aide d’un Comparateur personnalisé .

Un résumé de ses performances:

  • TreeMap fournit une performance de O (log (n)) pour la plupart des opérations

comme add () , remove () et contains () ** Un Treemap peut économiser de la mémoire (par rapport à HashMap) car il

utilise uniquement la quantité de mémoire nécessaire pour contenir ses éléments, contrairement à un HashMap qui utilise une région de mémoire contiguë ** Un arbre doit maintenir son équilibre afin de conserver sa destination

performances, cela nécessite un effort considérable, complique donc la mise en œuvre

Nous devrions aller pour un TreeMap chaque fois que:

  • les limites de mémoire doivent être prises en compte

  • nous ne savons pas combien d’articles doivent être stockés en mémoire

  • nous voulons extraire des objets dans un ordre naturel

  • si les articles seront systématiquement ajoutés et supprimés

  • nous sommes prêts à accepter O (log n) temps de recherche

4. Similitudes

4.1. Éléments uniques

TreeMap et HashMap ne prennent pas en charge les clés en double. Si ajouté, il remplace l’élément précédent (sans erreur ni exception):

@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. Accès simultané

  • Les deux implémentations de Map ne sont pas synchronisées ** et nous devons gérer nous-mêmes les accès simultanés.

Les deux doivent être synchronisés en externe chaque fois que plusieurs threads y accèdent simultanément et qu’au moins un des threads les modifie.

Nous devons explicitement utiliser Collections.synchronizedMap (mapName) pour obtenir une vue synchronisée d’une carte fournie.

4.3. Itérateurs Fail-Fast

Le Iterator lève une ConcurrentModificationException si le Map est modifié de quelque façon que ce soit et à tout moment une fois que l’itérateur a été créé.

De plus, nous pouvons utiliser la méthode remove de l’itérateur pour modifier le Map pendant l’itération.

Voyons un exemple:

@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. Quelle implémentation utiliser?

En général, les deux implémentations ont leurs avantages et inconvénients respectifs, cependant , il s’agit de comprendre les attentes et les exigences sous-jacentes qui doivent régir notre choix à l’égard de la même chose .

Résumant:

  • Nous devrions utiliser un TreeMap si nous voulons garder nos entrées triées

  • Nous devrions utiliser un HashMap si nous privilégions la performance à la mémoire

consommation ** Puisqu’un TreeMap a une localité plus importante, on pourrait envisager

si nous voulons accéder à des objets relativement proches les uns des autres selon leur ordre naturel ** HashMap peut être réglé à l’aide de initialCapacity et loadFactor ,

ce qui n’est pas possible pour le TreeMap ** Nous pouvons utiliser le LinkedHashMap si nous voulons conserver l’ordre d’insertion

tout en bénéficiant d’un accès à temps constant

6. Conclusion

Dans cet article, nous avons montré les différences et les similitudes entre TreeMap et HashMap .

Comme toujours, les exemples de code pour cet article sont disponibles à l’adresse over sur GitHub .