:: Enseignements :: Master :: M1 :: 2023-2024 :: Java Avancé ::
[LOGO]

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.

Tout document papier est proscrit.
La javadoc 21 est https://igm.univ-mlv.fr/~juge/javadoc-21/.
Les seuls documents électroniques autorisés sont les supports de cours à l'URL http://igm.univ-mlv.fr/~forax/ens/java-avance/cours/pdf/.

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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.