Une introduction à la classe Java.util.Hashtable

1. Vue d’ensemble

  • Hashtable est la plus ancienne implémentation d’une structure de données de table de hachage en Java. ** Le HashMap est la deuxième implémentation, qui a été introduit dans JDK 1.2.

Les deux classes offrent des fonctionnalités similaires, mais il existe également de petites différences que nous allons explorer dans ce tutoriel.

2. Quand utiliser Hashtable

Disons que nous avons un dictionnaire, où chaque mot a sa définition.

Nous devons également obtenir, insérer et supprimer des mots du dictionnaire rapidement.

Par conséquent, Hashtable (ou HashMap ) a un sens. Les mots seront les clés de la table de hachage, car ils sont supposés être uniques. Les définitions, par contre, seront les valeurs.

3. Exemple d’utilisation

Continuons avec l’exemple du dictionnaire. Nous allons modéliser Word comme une clé:

public class Word {
    private String name;

    public Word(String name) {
        this.name = name;
    }

   //...
}

Disons que les valeurs sont Strings . Maintenant nous pouvons créer un Hashtable :

Hashtable<Word, String> table = new Hashtable<>();

Commençons par ajouter une entrée:

Word word = new Word("cat");
table.put(word, "an animal");

Aussi, pour obtenir une entrée:

String definition = table.get(word);

Enfin, supprimons une entrée:

definition = table.remove(word);

Il y a beaucoup plus de méthodes dans la classe et nous en décrirons certaines plus tard.

Mais d’abord, parlons de certaines exigences pour l’objet clé.

4. L’importance de hashCode ()

  • Pour être utilisé comme clé dans un Hashtable , l’objet ne doit pas violer le lien:/java-hashcode[ hashCode () contract.]** En résumé, les objets identiques doivent renvoyer le même code. Pour comprendre pourquoi, regardons comment la table de hachage est organisée.

Hashtable utilise un tableau. Chaque position dans le tableau est un «compartiment» qui peut être nul ou contenir une ou plusieurs paires clé-valeur. L’indice de chaque paire est calculé.

Mais pourquoi ne pas stocker les éléments de manière séquentielle, en ajoutant de nouveaux éléments à la fin du tableau?

Le fait est que trouver un élément par index est beaucoup plus rapide que de parcourir les éléments avec la comparaison de manière séquentielle. Par conséquent, nous avons besoin d’une fonction qui mappe les clés aux index.

4.1. Tableau d’adresses directes

L’exemple le plus simple d’un tel mappage est la table d’adresses directes. Ici, les clés sont utilisées comme index:

index(k)=k,
where k is a key

Les clés sont uniques, chaque compartiment contient une paire clé-valeur. Cette technique fonctionne bien pour les clés entières lorsque leur plage possible est raisonnablement petite.

Mais nous avons deux problèmes ici:

  • Premièrement, nos clés ne sont pas des entiers, mais des objets Word

  • Deuxièmement, s’ils étaient des nombres entiers, personne ne garantirait qu’ils étaient petits.

Imaginez que les clés soient 1, 2 et 1000000. Nous aurons un grand tableau de taille 1000000 avec seulement trois éléments et le reste sera un espace perdu.

La méthode hashCode () résout le premier problème.

La logique de manipulation des données dans la Hashtable résout le deuxième problème.

Voyons cela en profondeur.

4.2. hashCode () Méthode

Tout objet Java hérite de la méthode hashCode () qui renvoie une valeur int . Cette valeur est calculée à partir de l’adresse de mémoire interne de l’objet. Par défaut, hashCode () renvoie des entiers distincts pour des objets distincts.

Ainsi tout objet clé peut être converti en un entier à l’aide de hashCode () .

Mais cet entier peut être grand.

4.3. Réduire la portée

Les méthodes get () , put () et remove () contiennent le code qui résout le deuxième problème - réduire la plage d’entiers possibles.

La formule calcule un index pour la clé:

int index = (hash & 0x7FFFFFFF) % tab.length;

tab.length est la taille du tableau et hash est un nombre renvoyé par la méthode hashCode () de la clé.

Comme on peut le voir, index rappelle la division hash par la taille du tableau . Notez que des codes de hachage égaux produisent le même index.

4.4. Collisions

De plus, même des codes de hachage différents peuvent produire le même index . Nous appelons cela une collision. Pour résoudre les conflits, Hashtable enregistre une LinkedList de paires clé-valeur.

Cette structure de données s’appelle une table de hachage avec chaînage.

4.5. Facteur de charge

Il est facile de deviner que les collisions ralentissent les opérations avec des éléments.

Pour obtenir une entrée, il ne suffit pas de connaître son index, nous devons parcourir la liste et comparer chaque élément.

Par conséquent, il est important de réduire le nombre de collisions. Plus le tableau est grand, plus le risque de collision est faible. Le facteur de charge détermine l’équilibre entre la taille du tableau et les performances. Par défaut, il est de 0,75, ce qui signifie que la taille du tableau double lorsque 75% des compartiments ne sont plus vides. Cette opération est exécutée par la méthode rehash () .

Mais revenons aux clés.

4.6. Remplacer equals () et hashCode ()

Lorsque nous mettons une entrée dans un Hashtable et la récupérons, nous nous attendons à ce que la valeur puisse être obtenue non seulement avec la même instance de la clé, mais aussi avec une clé égale

Word word = new Word("cat");
table.put(word, "an animal");
String extracted = table.get(new Word("cat"));
  • Pour définir les règles d’égalité, nous substituons la méthode equals () de la clé: **

public boolean equals(Object o) {
    if (o == this)
        return true;
    if (!(o instanceof Word))
        return false;

    Word word = (Word) o;
    return word.getName().equals(this.name);
}

Mais si nous ne remplaçons pas hashCode () lorsque equals () est redéfini, deux clés égales peuvent se retrouver dans des compartiments différents, car Hashtable calcule l’index de la clé à l’aide de son code de hachage.

Examinons de près l’exemple ci-dessus. Que se passe-t-il si nous ne remplaçons pas hashCode () ?

  • Deux instances de Word sont impliquées ici - la première est pour mettre

l’entrée et la seconde est pour obtenir l’entrée. Bien que ces les instances sont égales, leur méthode hashCode () renvoie des nombres différents ** L’indice de chaque clé est calculé à l’aide de la formule de la section 4.3.

Selon cette formule, différents codes de hachage peuvent produire différents index ** Cela signifie que nous mettons l’entrée dans un seau et essayons ensuite d’obtenir

sortir de l’autre seau. Une telle logique casse Hashtable

Les clés Equal doivent renvoyer des codes de hachage égaux, c’est pourquoi nous substituons la méthode hashCode () : **

public int hashCode() {
    return name.hashCode();
}

Notez qu’il est également recommandé de faire en sorte que les clés différentes ne renvoient pas des codes de hachage différents, sinon elles se retrouveraient dans le même compartiment. Cela va frapper la performance, perdant ainsi certains des avantages d’une table de hachage.

Notez également que nous ne nous soucions pas des clés de String , Integer , Long ou d’un autre type d’encapsuleur Les méthodes equal () et hashCode () sont déjà remplacées dans les classes wrapper.

5. Itération Hashtables

Il existe plusieurs façons de parcourir les tables __Hashtables. __Dans cette section, parlez-en et expliquez certaines de leurs implications.

5.1. Fail Fast: Iteration

L’itération Fail-fast signifie que si un Hashtable est modifié après la création de son _Iterator , alors le ConcurrentModificationException_ sera levé. Voyons cela.

Premièrement, nous allons créer un Hashtable et y ajouter des entrées:

Hashtable<Word, String> table = new Hashtable<Word, String>();
table.put(new Word("cat"), "an animal");
table.put(new Word("dog"), "another animal");

Deuxièmement, nous allons créer un Iterator :

Iterator<Word> it = table.keySet().iterator();

Et troisièmement, nous allons modifier le tableau:

table.remove(new Word("dog"));

Si nous essayons de parcourir la table, nous obtiendrons une ConcurrentModificationException :

while (it.hasNext()) {
    Word key = it.next();
}
java.util.ConcurrentModificationException
    at java.util.Hashtable$Enumerator.next(Hashtable.java:1378)

ConcurrentModificationException permet de rechercher les bogues et d’éviter ainsi les comportements imprévisibles, par exemple lorsqu’un thread parcourt la table et qu’un autre tente de le modifier en même temps.

5.2. Pas d’échec rapide: Enumeration

Enumeration dans un Hashtable n’est pas rapide. Regardons un exemple.

Commençons par créer une table de hachage et y ajouter des entrées:

Hashtable<Word, String> table = new Hashtable<Word, String>();
table.put(new Word("1"), "one");
table.put(new Word("2"), "two");

Deuxièmement, créons un Enumeration :

Enumeration<Word> enumKey = table.keys();

Troisièmement, modifions le tableau:

table.remove(new Word("1"));

Maintenant, si nous parcourons la table, il ne jettera pas une exception:

while (enumKey.hasMoreElements()) {
    Word key = enumKey.nextElement();
}

5.3. Ordre d’itération imprévisible

En outre, notez que l’ordre d’itération dans un Hashtable est imprévisible et ne correspond pas à l’ordre dans lequel les entrées ont été ajoutées.

Cela est compréhensible car il calcule chaque index en utilisant le code de hachage de la clé. De plus, le rechapage a lieu de temps en temps, réarrangeant l’ordre de la structure de données.

Par conséquent, ajoutons quelques entrées et vérifions le résultat:

Hashtable<Word, String> table = new Hashtable<Word, String>();
    table.put(new Word("1"), "one");
    table.put(new Word("2"), "two");
   //...
    table.put(new Word("8"), "eight");

    Iterator<Map.Entry<Word, String>> it = table.entrySet().iterator();
    while (it.hasNext()) {
        Map.Entry<Word, String> entry = it.next();
       //...
    }
}
five
four
three
two
one
eight
seven

6. Hashtable contre HashMap

Hashtable et HashMap fournissent des fonctionnalités très similaires.

Les deux fournissent:

  • Itération rapide

  • Ordre d’itération imprévisible

Mais il y a aussi des différences:

  • HashMap ne fournit aucune Enumeration, tandis que Hashtable fournit

pas échec-rapide Enumeration ** Hashtable n’autorise pas les clés null et les valeurs null , alors que

HashMap autorise une clé null et un nombre quelconque de valeurs null ** Les méthodes de Hashtable sont synchronisées tandis que celles de HashMaps sont

ne pas

7. Hashtable API en Java 8

Java 8 a introduit de nouvelles méthodes permettant de rendre notre code plus propre. En particulier, nous pouvons nous débarrasser de certains if blocs. Voyons cela.

7.1. getOrDefault ()

Disons que nous devons obtenir la définition du mot «dog _ et l’assigner à la variable si elle se trouve sur la table. Sinon, affectez «non trouvé» à la variable.

Avant Java 8:

Word key = new Word("dog");
String definition;

if (table.containsKey(key)) {
     definition = table.get(key);
} else {
     definition = "not found";
}

Après Java 8:

definition = table.getOrDefault(key, "not found");

7.2. putIfAbsent ()

Disons que nous devons mettre le mot "chat _" _ seulement si ce n’est pas encore dans le dictionnaire.

Avant Java 8:

if (!table.containsKey(new Word("cat"))) {
    table.put(new Word("cat"), definition);
}

Après Java 8:

table.putIfAbsent(new Word("cat"), definition);

7.3. boolean remove ()

Disons que nous devons supprimer le mot «chat», mais seulement si sa définition est «un animal».

Avant Java 8:

if (table.get(new Word("cat")).equals("an animal")) {
    table.remove(new Word("cat"));
}

Après Java 8:

boolean result = table.remove(new Word("cat"), "an animal");

Enfin, alors que l’ancienne méthode remove () renvoie la valeur, la nouvelle méthode renvoie boolean .

7.4. remplacer()

Supposons que nous devions remplacer la définition de «chat», mais seulement si son ancienne définition était «un petit mammifère carnivore domestique».

Avant Java 8:

if (table.containsKey(new Word("cat"))
    && table.get(new Word("cat")).equals("a small domesticated carnivorous mammal")) {
    table.put(new Word("cat"), definition);
}

Après Java 8:

table.replace(new Word("cat"), "a small domesticated carnivorous mammal", definition);

7.5. computeIfAbsent ()

Cette méthode est similaire à putIfabsent () . Mais putIfabsent () prend directement la valeur et computeIfAbsent () prend une fonction de mappage. Il calcule la valeur uniquement après avoir vérifié la clé, ce qui est plus efficace, en particulier si la valeur est difficile à obtenir.

table.computeIfAbsent(new Word("cat"), key -> "an animal");

Par conséquent, la ligne ci-dessus est équivalente à:

if (!table.containsKey(cat)) {
    String definition = "an animal";//note that calculations take place inside if block
    table.put(new Word("cat"), definition);
}

7.6. computeIfPresent ()

Cette méthode est similaire à la méthode replace () . Mais, encore une fois, replace () prend la valeur directement et computeIfPresent () prend une fonction de mappage. Il calcule la valeur à l’intérieur du bloc if , c’est pourquoi il est plus efficace.

Disons que nous devons changer la définition:

table.computeIfPresent(cat, (key, value) -> key.getName() + " - " + value);

Par conséquent, la ligne ci-dessus est équivalente à:

if (table.containsKey(cat)) {
    String concatination=cat.getName() + " - " + table.get(cat);
    table.put(cat, concatination);
}

7.7. calculer()

Maintenant, nous allons résoudre une autre tâche. Disons que nous avons un tableau de String , où les éléments ne sont pas uniques. Calculons également le nombre d’occurrences d’une chaîne que nous pouvons obtenir dans le tableau. Voici le tableau:

String[]animals = { "cat", "dog", "dog", "cat", "bird", "mouse", "mouse" };

De plus, nous voulons créer un Hashtable qui contient un animal en tant que clé et le nombre de ses occurrences en tant que valeur.

Voici une solution:

Hashtable<String, Integer> table = new Hashtable<String, Integer>();

for (String animal : animals) {
    table.compute(animal,
        (key, value) -> (value == null ? 1 : value + 1));
}

Enfin, assurez-vous que la table contient deux chats, deux chiens, un oiseau et deux souris:

assertThat(table.values(), hasItems(2, 2, 2, 1));

7.8. fusionner()

Il existe un autre moyen de résoudre la tâche ci-dessus:

for (String animal : animals) {
    table.merge(animal, 1, (oldValue, value) -> (oldValue + value));
}

Le deuxième argument, 1 , est la valeur qui est mappée à la clé si la clé n’est pas encore sur la table. Si la clé est déjà dans la table, nous la calculons comme suit: oldValue 1 .

7.9. pour chaque()

C’est une nouvelle façon de parcourir les entrées. Imprimons toutes les entrées:

table.forEach((k, v) -> System.out.println(k.getName() + " - " + v)

7.10. remplace tout()

De plus, nous pouvons remplacer toutes les valeurs sans itération:

table.replaceAll((k, v) -> k.getName() + " - " + v);

8. Conclusion

Dans cet article, nous avons décrit l’objet de la structure de la table de hachage et montré comment compliquer l’obtention d’une structure de table à adresse directe.

De plus, nous avons expliqué ce que sont les collisions et le facteur de charge dans une Hashtable. Nous avons également appris pourquoi remplacer equals () et hashCode () pour des objets clés.

Enfin, nous avons parlé des propriétés de Hashtable et de l’API spécifique à Java 8.

Comme d’habitude, le code source complet est disponible à l’adresse on Github .