:: Enseignements :: Master :: M1 :: 2023-2024 :: Java Avancé ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) | Examen session 2 de Java Avancé 2023-2024 |
Le but de ce TP noté est de créer une liste DedupVec capable de "dé-dupliquer" des objets.
Vos sources Java produites pendant l'examen devront être placées sous le répertoire EXAM
de votre compte ($HOME) (qui est vide dans l'environnement de TP noté).
Sinon, elles ne seront pas récupérées.
Comme nous utilisons Java 21, vous devez configurer votre Eclipse pour cela :
Dans Window > Preferences > Java > Installed JREs: vous devez ajouter Java 21 qui est disponible sur vos machines
dans le répertoire /usr/local/apps/java21
Dans Window > Preferences > Java > Compiler le niveau compiler compliance level doit être 21.
Vous avez le droit de lire le sujet jusqu'au bout, cela vous donnera une bonne idée de là où on veut aller !
Exercice 1 - DedupVec
DedupVec est une liste dynamique qui, lorsque l'on stocke deux objets égaux au sens de
equals,
stocke une référence sur la première instance à la place de la seconde.
Par exemple, on déclare deux points
point1 et
point2 ayant les mêmes valeurs
record Point(int x, int y) {}
...
Point point1 = new Point(2, 3);
Point point2 = new Point(2, 3);
Si l'on ajoute successivement les deux points à un
DedupVec,
DedupVec<Point> dedupVec = new DedupVec<Point>();
dedupVec.add(point1);
dedupVec.add(point2);
lors de l'ajout de
point2, l'implantation de
DedupVec se rend compte qu'il existe déjà
dans
DedupVec une instance de
Point égale à
point2 et substitue
point2
par
point1. Les deux points stockés dans
DedupVec ont alors la même adresse.
On peut tester cela en utilisant l'opérateur
== sur les références.
point1.equals(dedupVec.get(0)) // true
point1 == dedupVec.get(0) // true
point2.equals(dedupVec.get(1)) // true
point2 == dedupVec.get(1) // false
point1 == dedupVec.get(1) // true
Le fait de remplacer une instance par une autre ayant les mêmes valeurs est appelé
dé-duplication. En Java, certains
garbage collectors, entre autres G1, le
garbage collector par défaut,
font cela automatiquement pour le contenu des Strings.
En terme d'implantation, la classe
DedupVec contient 2 champs.
Le premier est une instance de l'interface
java.util.List qui stocke les éléments
et le second est une instance de l'interface
java.util.Map qui associe à un élément
que l'on a déjà vu, le même élément, comme cela, si l'on ajoute un élément égal (au sens de
equals)
à l'élément déjà vu, on peut le substituer.
Attention : si votre implantation n'a pas le bon nombre de champs (2) ou
qu'ils n'ont pas le bon type (sous-type de List et sous-type de Map),
votre implantation sera considérée comme hors sujet !
Le test unitaire
suddenDeath de la question 2 vérifie que votre implantation n'est pas hors sujet.
Dans l'exemple ci-dessous, après avoir ajouté
point1, la liste contient
point1
et la map associe
point1 à
point1. Lorsque l'on ajoute
point2, comme
il est egal à
point1, la liste contient maintenant deux fois
point1 et la map
associe toujours
point1 à
point1.
La classe
DedupVec représente une liste d'éléments
non-null qui possède les méthodes suivantes
-
size qui renvoie le nombre d'éléments,
-
get(index) renvoie l'élément à l'index index,
-
add(element) qui ajoute un élément (ou un élément égal déjà présent).
-
contains(element) qui indique si element est un élément de la liste
-
addAll(dedupVec) qui ajoute tous les éléments du dedupVec pris en paramètre
dans le DedupVec courant.
-
fromSet(set) qui prend un ensemble en paramètre et créé le DedupVec correspondant.
Des tests unitaires correspondant à l'implantation sont ici :
DedupVecTest.java
Note : comme on utilise les tests unitaires JUnit sans Maven, dans la configuration de votre projet, il faut ajouter
la librairie JUnit 5, soit à partir du fichier
DedupVecTest.java, en cliquant sur l'annotation
@Test et en sélectionnant le quickfix "Fixup project ...", soit en sélectionnant les "Properties" du projet
(avec le bouton droit de la souris sur le projet) puis en ajoutant la librairie JUnit 5 (jupiter) au ClassPath.
-
On va dans un premier temps écrire une version simple de la classe DedupVec, avec un constructeur
sans paramètre, les méthodes size et get(index) ainsi que la méthode add(element)
qui pour l'instant n'effectue pas la dé-duplication.
Quelle implantation de l'interface List allez-vous choisir pour stocker les éléments ?
Implanter la classe DedupVec.
Vérifier que les tests marqués "Q1" passent.
-
On souhaite maintenant que lorsque l'on ajoute un élément, l'implantation effectue la dé-duplication
comme décrit ci-dessus.
Quelle implantation de l'interface Map allez-vous choisir ?
Modifier l'implantation de la classe DedupVec pour que la dé-duplication des instances
soit faite.
-
On souhaite ajouter une méthode contains(element) qui teste si une valeur est un des éléments
de DedupVec.
Sachant que l'on veut une complexité pire cas en temps constant, comment doit-on faire ?
Implanter la méthode contains(element).
Vérifier que les tests marqués "Q3" passent.
-
On souhaite ajouter une méthode addAll(dedupVec) qui permet d'ajouter tous les éléments
d'un DedupVec à un autre DedupVec.
Quelle doit être la signature (le type des paramètres) d'une telle méthode ?
Implanter la méthode addAll.
Vérifier que les tests marqués "Q4" passent.
-
On souhaite ajouter une méthode d'aide (helper method) pas publique, newMapFromSet(set)
qui prend en paramètre un ensemble et renvoie une Map qui pour chaque élément de l'ensemble
associe ce même élément.
Par exemple, si l'ensemble contient uniquement la chaîne de caractères "toto", on obtient une Map map
telle que si on appelle map.get("toto"), le résultat est "toto".
Pour des questions d'efficacité (cf. question suivante) la Map renvoyée doit être une vue
de l'ensemble pris en paramètre et si l'ensemble pris en paramètre contient la valeur null,
l'appel à la méthode newMapFromSet(set) ne devra pas lever d'exception et c'est uniquement
lorsque l'on essaye d'accéder à l'élément qu'une exception NullPointerException devra être levée.
Sachant qu'il existe la classe AbstractMap, écrire la méthode newMapFromSet(set).
Vérifier que les tests marqués "Q5" passent.
-
On va maintenant écrire la méthode fromSet(set) qui prend en paramètre un ensemble et créé
un DedupVec avec l'ensemble des éléments de l'ensemble.
Cette méthode doit être plus efficace que de créer un DedupVec puis de faire des add
des éléments du Set car par définition, un ensemble ne contient pas d'éléments dupliqués,
donc il n'est pas nécessaire de dé-dupliquer les éléments à la création.
Vérifier que les tests marqués "Q6" passent.
Note : une fois créé par la méthode fromSet(set), le DedupVec se comporte de la même
façon qu'un DedupVec créé en appelant le constructeur.
-
On souhaite changer l'implantation de addAll pour qu'elle soit un peu plus efficace (mais
pas de quoi changer la complexité pire cas malheureusement).
On peut remarquer que si l'on utilise un DedupVec plutôt qu'une liste classique,
c'est parce qu'il y a de grandes chances qu'il y ait des doublons. Dans ce cas, l'ensemble des valeurs
dans la Map est sûrement plus petit que les éléments dans la liste.
On peut alors optimiser pour le cas où les ensembles des valeurs dans les Map des deux DedupVec
sont disjoints. En effet, si c'est le cas, on peut directement faire un addAll des éléments
des listes car on n'est sûr qu'il n'y a pas de doublons créés par un élément qui serait dans chacun des deux DedupVec.
On va donc utiliser l'algorithme suivant :
addAll(dedupVec1, dedupVec2)
on initialise un drapeau à faux
pour chaque valeur de l'ensemble des clés de la map de dedupVec2
on ajoute la clé à la map de dedupVec1 si la clé n'existe pas déjà
si la clé existe déjà, on met le drapeau à vrai
// Note : en Java, il existe une méthode putIfAbsent qui fait les 2 lignes précédentes en un seul appel
// il suffit de regarder la valeur de retour de putIfAbsent
si le drapeau est faux
on peut ajouter tous les éléments de la liste de dedupVec2 dans la liste de dedupVec1
sinon
pour chaque élément de la liste de dedupVec2
on va rechercher l'élément dans la map de dedupVec1
on ajoute le résultat dans la liste de dedupVec1
Commenter votre précédente méthode addAll et écrire une nouvelle implantation.
Note : Il n'y a pas de tests supplémentaires pour cette question, car c'est une optimisation,
les tests précédents doivent continuer à fonctionner.
-
On souhaite que la classe DedupVec se comporte comme une java.util.List.
Pour nous aider, nous allons utiliser la classe AbstractList qui fournit déjà
une implantation de nombreuses méthodes de l'interface List.
On a un petit problème avec addAll, car la méthode addAll que l'on a déjà
écrite et celle de l'interface List n'ont pas la même signature.
On va garder la signature de la méthode addAll de List mais
on va tester dynamiquement si la collection prise en paramètre n'est pas un DedupVec pour
utiliser l'algorithme que l'on a écrit à la question précédente pour ne pas perdre en efficacité.
Faire les changements qui s'imposent pour que DedupVec implante l'interface List.
Vérifier que les tests marqués "Q8" passent.
-
Enfin, on peut remarquer que sur DedupVec, il n'est pour l'instant pas possible d'ajouter un élément
autre part qu'à la fin, une méthode comme addFirst ou un appel à la méthode add
sur une subList() ne marchent pas.
En relisant la documentation de la classe AbstractList, quelle méthode (une seule) doit-on
implanter pour que ces méthodes fonctionnent ?
Implanter la méthode nécessaire.
Vérifier que les tests marqués "Q9" passent.
© Université de Marne-la-Vallée