:: Enseignements :: ESIPE :: E4INFO :: 2026-2027 :: Java Avancé ::
[LOGO]

Hacher menu


Implantation d'une table de hachage, classe interne, type paramétré.
Le but de ces deux exercices est d'implanter une représentation d'un ensemble d'éléments (de n'importe quel type) sans doublons.
Comme les éléments ne sont pas forcément consécutifs, il n'est pas possible d'utiliser un tableau ou un bitset pour représenter les valeurs (explosion en mémoire) ni une liste (algos d'ajout/recherche/suppression en O(n)), on utilisera donc une table de hachage où les collisions sont stockées dans une liste chaînée.
La taille de la table de hachage sera toujours une puissance de 2 (2, 4, 8, 16, 32, etc...) pour permettre l'écriture d'une fonction de hachage rapide.
Le but du TD est de ré-écrire une table de hachage, pas d'en utiliser une qui existe déjà, donc pour ce TD, nous n'utiliserons aucune des collections de java.util.

Exercice 1 - Maven

Pour ce TP, nous allons utiliser Maven avec une configuration (le pom.xml) très similaire à celle habituelle.
       <project xmlns="http://maven.apache.org/POM/4.0.0"
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

         <modelVersion>4.0.0</modelVersion>
         <groupId>fr.uge.set</groupId>
         <artifactId>set</artifactId>
         <version>0.0.1-SNAPSHOT</version>

         <properties>
           <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         </properties>

         <dependencies>
             <dependency>
                 <groupId>org.junit.jupiter</groupId>
                 <artifactId>junit-jupiter-api</artifactId>
                 <version>5.14.4</version>
                 <scope>test</scope>
             </dependency>
         </dependencies>

         <build>
             <plugins>
                 <plugin>
                     <groupId>org.apache.maven.plugins</groupId>
                     <artifactId>maven-compiler-plugin</artifactId>
                     <version>3.16.0</version>
                     <configuration>
                         <release>27</release>
                     </configuration>
             </plugin>

             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-surefire-plugin</artifactId>
                 <version>3.6.0</version>
             </plugin>
         </plugins>
         </build>
       </project>
     
Comme précédemment, créer un projet Maven, au niveau du premier écran, cocher create simple project puis passer à l'écran suivant en indiquant Next.
Pour ce TP, le groupId est fr.uge.set , l'artefactId est set et la version est 0.0.1-SNAPSHOT. Puis cliquer sur Finish.

Exercice 2 - HashTableSet

Dans ce TP, une table de hachage est un tableau contenant des listes chaînées. Lorsque l'on veut insérer/rechercher un élément, on va dans un premier temps trouver dans quelle case du tableau doit se situer l'élément puis dans un second temps, on va parcourir la liste chaînée. En cas d'insertion, si l'élément n'existe pas, on va l'insérer en tête de la liste, sinon on ne fait rien. En cas de recherche, on renvoie vrai si l'élément fait partie de la liste chaînée.
Pour trouver la case de tableau, on va demander à l'élément de renvoyer son hashCode, puis on choisira la case du tableau avec une opération de modulo (% comme en C).
Par exemple, si on a un tableau de 4 cases, et que l'on veut insérer "Jane", le hashCode de "Jane" est 2301262, modulo 4 cela fait 2, donc il faut ajouter dans la liste chaînée à la case 2, un maillon (Entry) contenant "Jane".
On obtient la table de hachage suivante
     0:  [null]
     1:  [null]
     2:  [] -> Entry("Jane", null)
     3:  [null]
    
Si l'on veut maintenant insérer "John", le hashCode de "John" est 2314539, modulo 4, on obtient 3. On insère donc le maillon contenant "John" comme ceci
     0:  [null]
     1:  [null]
     2:  [] -> Entry("Jane", null)
     3:  [] -> Entry("John", null)
    
Enfin, si l'on veut ajouter "Arthur", la hashCode est 1969735650, modulo 4 cela fait 2, donc on ajoute le maillon contenant "Arthur" devant le maillon contenant "Jane".
     0:  [null]
     1:  [null]
     2:  [] -> Entry("Arthur", ) -> Entry("Jane", null)
     3:  [] -> Entry("John", null)
    

En pratique, on utilise rarement le modulo, car le hashCode peut être négatif et le modulo d'un nombre négatif est négatif. On s'arrange pour que la taille de la table soit une puissance de deux car dans ce cas, h % length est équivalent à h & (length - 1) (et % est aussi une opération assez lente, enfin comparée à &).
Reste à savoir comment on dimensionne le tableau en fonction du nombre d'éléments... Il y a plusieurs stratégies, celle considérée comme efficace, mais au détriment de la taille en mémoire, consiste à avoir toujours au moins 2 fois plus de cases du tableau que d'éléments.

Les tests JUnit 5 de cet exercice sont HashTableSetTest.java.

  1. Quels doivent être les champs de la classe Entry correspondant à un maillon des listes chaînées utilisées par table de hachage ?
    Rappeler quel est l'intérêt de déclarer Entry comme membre de la classe HashTableSet plutôt que comme une classe à côté dans le même package que HashTableSet ?
    Ne pourrait-on pas utiliser un record plutôt qu'une classe, ici ? Si oui, pourquoi ? Sinon, pourquoi ?
    Écrire la classe HashTableSet paramétrée par le type des éléments, dans le package fr.uge.set, son constructeur et ajouter Entry en tant que classe interne.
    Vérifier que les tests marqués "Q1" passent.

  2. On souhaite maintenant ajouter un constructeur sans paramètre, une méthode add qui permet d'ajouter un élément non nul et une méthode size qui renvoie le nombre d'éléments déjà insérés, avec une complexité en O(1).
    Pour l'instant, on va dire que la taille du tableau est toujours 16, on fera en sorte que la table de hachage s'agrandisse toute seule plus tard.
    Expliquer pourquoi en Java, on ne peut pas créer de tableau de type paramétré ? Comment allons-nous faire alors ?
    Dans la classe HashTableSet, implanter le constructeur et les méthodes add et size.
    Vérifier que les tests marqués "Q2" passent.
    ATTENTION : add ne doit pas permettre d'ajouter deux fois le même élément, un ensemble/set ne permet pas les doublons.

  3. On cherche maintenant à implanter une méthode forEach qui prend en paramètre une fonction. La méthode forEach parcourt tous les éléments insérés et pour chaque élément, appelle la fonction prise en paramètre avec l'élément courant.
    Quelle doit être la signature de la functional interface prise en paramètre de la méthode forEach ?
    Quel est le nom de la classe du package java.util.function qui a une méthode ayant la même signature ?
    Écrire la méthode forEach.
    Vérifier que les tests marqués "Q3" passent.
    Note : si vous êtes balèze, forEach peut s'écrire avec un Stream.

  4. En fait, la signature de la méthode forEach que vous avez écrite n'est peut-être pas la bonne. En effet, forEach est une méthode publique qui prend en paramètre un type paramétré, donc les règles PECS s'appliquent.
    Modifier la signature de votre méthode en conséquence.
    Vérifier que les tests marqués "Q4" passent.

  5. On veut maintenant faire en sorte que la table de hachage se redimensionne toute seule. Pour cela, lors de l'ajout d'un élément, on peut avoir à agrandir la table pour garder comme invariant que la taille du tableau est au moins 2 fois plus grande que le nombre d'éléments.
    Pour agrandir la table, on va créer un nouveau tableau deux fois plus grand et recopier tous les éléments dans ce nouveau tableau à la bonne place. Ensuite, il suffit de remplacer l'ancien tableau par le nouveau.
    Expliquer pourquoi, en plus d'être plus lisible, en termes de performance, l'agrandissement doit se faire dans sa propre méthode.
    Modifier votre implantation pour que la table s'agrandisse dynamiquement.
    Vérifier que les tests marqués "Q5" passent.
    Note : vous pouvez utiliser forEach pour parcourir les éléments de l'ancienne table.

  6. On souhaite maintenant ajouter une méthode contains qui renvoie si un objet pris en paramètre est un élément de l'ensemble ou pas, sous forme d'un booléen.
    Quel doit être le type du paramètre de contains() ?
    Expliquer pourquoi nous n'allons pas utiliser forEach pour implanter contains (Il y a deux raisons, une algorithmique et une spécifique à Java).
    Écrire la méthode contains.
    Vérifier que les tests marqués "Q6" passent.

  7. On souhaite maintenant écrire une méthode addAll qui permet d'ajouter tous les éléments d'un HashTableSet dans le HashTableSet courant.
    Écrire la méthode addAll.
    Vérifier que les tests marqués "Q7" passent.

  8. Enfin, pour les plus balèzes, on souhaite pouvoir tester si deux HashTableSet sont égaux avec la méthode equals.
    Modifier votre code en conséquence.
    Vérifier que les tests marqués "Q8" passent.