:: Enseignements :: Master :: M1 :: 2023-2024 :: Java Avancé ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) |
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, ils 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, ici, pas besoin d'activer les
preview features.
<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.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<release>21</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</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 la 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 à &).
Vous pouvez regarder la première page du
Hacker's Delight
si vous voulez comprendre pourquoi on peut écrire un % avec un & quand la taille est
une puissance de 2.
Reste à savoir comment on dimensionne le tableau en fonction du nombre d'éléments...
Il a plusieurs stratégies, celle considérée comme la plus 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.
Nous allons, dans un premier temps, implanter un ensemble utilisant
une table de hachage d'Object puis dans un second temps,
nous introduirons la notion de type paramétré.
-
Quels doivent être les champs de la classe Entry correspondant à une case
d'une des listes chaînées utilisées par table de hachage
Note : on va pas utiliser java.util.LinkedList, car on veut une liste
simplement chaînée.
Rappeler quelle est l'intérêt de déclarer Entry comme membre
de la classe HashTableSet plutôt que comme une classe à coté
dans le même package que HashTableSet ?
Ne pourrait-on pas utiliser un record plutôt qu'une classe, ici ? Si oui, pourquoi ?
Si non, pourquoi ?
Écrire la classe HashTableSet dans le package fr.uge.set et
ajouter Entry en tant que classe interne.
Vérifier que les tests marqués "Q1" passent.
-
On souhaite maintenant ajouter un constructeur sans paramètre,
une méthode add qui permet d'ajouter un élément non null
et une méthode size qui renvoie le nombre d'éléments 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.
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.
-
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.
-
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.
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 "Q4" passent.
-
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 touts 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.
-
L'implantation actuelle a un problème : même si on n'ajoute que des String
lorsque l'on utilise forEach, l'utilisateur va probablement devoir faire des cast parce que les éléments envoyés par forEach sont typés Object.
Par exemple, si on veut vérifier que toutes les chaînes de caractères contenues dans
un ensemble commencent par "f", le code ci-dessous ne fonctionne pas
var set = new HashTableSet();
set.add("foo");
set.add("five");
set.add("fallout");
set.forEach(element -> assertTrue(element.startsWith("f")));
Pour que ce code fonctionne, il faut dire que le type des éléments que l'on ajoute avec
add et le type des éléments que l'on reçoit dans la lambda du forEach
est le même. Pour cela, il faut déclarer HashTableSet comme un type paramétré.
Rappeler pourquoi en Java, il n'est pas possible de créer un tableau de
type paramétré ? Quel est le work around ?
Pourquoi celui-ci génère-t-il un warning ?
Et dans quel cas et comment peut on supprimer ce warning ?
Mettez en commentaire votre ancien code, puis dupliquez-le pour faire
les changements qui permettent d'avoir un ensemble paramétré par le type des ses éléments.
Vérifier que les tests marqués "Q6" passent.
Attention: il faut que les tests écrits précédemment continuent de compiler (avec des warnings),
donc les changements que vous effectuez doivent être rétro-compatibles avec l'ancienne
version de votre code.
-
En fait, la signature de la méthode forEach que vous avez écrite n'est pas la bonne.
En effet, forEach appelle la lambda avec des éléments de type E, donc
la fonction peut prendre en paramètre des valeurs qui sont des super-types de E.
Regarder comme on déclare un super-type dans un type paramétré en regardant la javadoc de la méthode
forEach
de ArrayList.
Modifier votre code en conséquence.
Vérifier que les tests marqués "Q7" passent.
-
On souhaite maintenant écrire une méthode addAll qui permet d'ajouter
tous les éléments d'un HashTableSet dans le HashTableSet courant.
Note : les éléments du HashTableSet pris en paramètre peuvent être des sous-types
du type du HashTableSet courant. Si vous ne savez pas comment déclarer
un sous-type dans un type paramétré, vous pouvez regarder la méthode
Collection.addAll()
des API du JDK.
Écrire la méthode addAll.
Vérifier que les tests marqués "Q8" passent.
-
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 "Q9" passent.
© Université de Marne-la-Vallée