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

Examen session 1 de Java Avancé 2023


Le but de ce TP noté est de créer une interface Expando et sa classe utilitaire ExpandoUtils permettant de déclarer des objets non-mutables ayant des propriétés typées et des propriétés non typées.

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 donc 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 - Expando

Expando est une interface implantée par des records qui veulent avoir à la fois des propriétés typées et des propriétés non typées.
L'interface est la suivante :
 public interface Expando {
   Map<String, Object> moreAttributes();
 }
     

Si par exemple, on souhaite définir une Person comme étant un Expando, on va écrire le record suivant
record Person(String name, Map<String, Object> moreAttributes) implements Expando {
  Person {
    Objects.requireNonNull(name);
    moreAttributes = ExpandoUtils.copyAttributes(moreAttributes, Person.class);
  }
}

var john = new Person("John", Map.of("age", 32));
     
Le record Person implante l'interface Expando et possède comme deuxième composant, le composant moreAttributes qui permet de stocker les valeurs non typées. Ici, une Person possède un nom (name) ainsi que d'autres attributs (moreAttributes) qui peuvent avoir n'importe quelle clé sauf "name".
La méthode copyAttributes assure que les valeurs non typées ne peuvent pas être modifiées (en faisant une copie si nécessaire) et aussi que les noms des attributs de la Map ne sont pas un des noms des composants du record. De plus, les clés et valeurs de moreAttributes ne peuvent pas être null.
Par exemple, il n'est pas possible de créer l'instance de Person suivante
      new Person("John", Map.of("name", "Jane"));
     
car Person définit la propriété "name" comme une propriété typée donc elle ne peut pas apparaître en tant que propriété non typée dans moreAttributes.
Note : le composant moreAttributes du record n'est pas considéré comme contenant une propriété typée donc "moreAttributes" est un nom valide en tant que clé de la Map.
Pour la suite du sujet, on considérera que les Expando sont toujours bien écrits, avec au niveau du record le composant moreAttributes défini et la méthode copyAttributes appelée correctement dans le constructeur.

Des tests unitaires correspondant à l'implantation sont ici : ExpandoTest.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 ExpandoTest.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. Dans un premier temps, on souhaite créer la méthode copyAttributes(attributes, type) dans la classe ExpandoUtils. Cette méthode prend en paramètre une Map qui associe une valeur à une chaîne de caractères, et la classe du record. Elle
    • s'assure que les noms des attributs et leurs valeurs ne sont pas null ;
    • s'assure que le nom de chaque attribut n'est pas le nom d'un composant (moreAttributes exclu) ;
    • renvoie une Map non mutable.

    Écrire la méthode copyAttributes(attribute, type).
    Vérifier que les tests marqués "Q1" passent.
    Rappel : il existe une méthode getRecordComponents sur java.lang.Class qui renvoie tous les composants d'un record.
    Note : Faites attention à la complexité des opérations que vous effectuez. Le nombre de composants d'un record est limité (pas plus de 255 composants), alors que les attributs non-typés peuvent être nombreux.

  2. Si vous ne l'avez pas déjà fait, la méthode Class.getRecordComponents() est lente donc on va vouloir "mettre en cache" en utilisant un ClassValue. On pourrait juste mettre en cache le tableau des composants des records, mais par la suite, on va vouloir une Map qui associe à un nom d'un composant de record, le composant du record associé (une Map<String, RecordComponent>).
    Dans ExpandoUtils, créer une constante de type ClassValue mettant en cache la Map qui associe à un nom d'un composant de record, le composant du record associé (en retirant moreAttributes). Pour créer cette Map, vous utiliserez un Stream.
    Puis modifier l'implantation de copyAttributes, en utilisant cette constante dans le but d'améliorer les performances de cette méthode.
    Vérifier que les tests marqués "Q2" passent.

  3. Si vous ne l'avez pas déjà fait, on souhaite améliorer la signature de la méthode copyAttributes dans deux directions, la signature doit être la plus générale possible (cf les règles PECS) et de plus, on veut s'assurer que la classe prise en paramètre est bien un record qui implante l'interface Expando.
    Vérifier que les tests marqués "Q3" passent. Pour le test, shouldNotCompile, les deux appels à la méthode copyAttributes à la fin de la méthode ne devrait pas compiler si vous les dé-commentez.
    Note: si vous n'y arrivez pas, vous pouvez passer à la question suivante.

  4. Avant de faire la question 5, on va créer une méthode d'aide (helper method), invoke(accessor, expando) dans ExpandoUtils qui sait exécuter l'accesseur d'un composant d'un record (une instance de java.lang.reflect.Method) sur un Expando et renvoyer la valeur de retour.
    Cette méthode doit gérer correctement les exceptions, à savoir
    • Propager une exception IllegalAccessError avec l'exception originelle si l'accesseur n'est pas accessible
    • Propager les exceptions non-checked renvoyées par l'appel à l'accesseur
    • Propager une exception AssertionError avec l'exception originelle si l'accesseur déclare une exception checked (ce qui n'est pas possible en Java).
    Écrire la méthode invoke(accessor, expando).
    Vérifier que les tests marqués "Q4" passent.

  5. On souhaite ajouter à l'interface Expando une méthode asMap() qui renvoie une vue non-mutable de cet Expando sous la forme d'une Map qui associe à un nom sa valeur, que celle-ci soit définie sous forme de valeur typée (en tant que composant du record) ou non typée (dans moreAttributes) de l'Expando.
    Par exemple, avec le même record Person
    record Person(String name, Map<String, Object> moreAttributes) implements Expando {
      Person {
       Objects.requireNonNull(name);
       moreAttributes = ExpandoUtils.copyAttributes(moreAttributes, Person.class);
      }
    }
    
    var jane = new Person("Jane", Map.of("age", 34));
    assertEquals(Map.of("name", "Jane", "age", 34), jane.asMap());  // true
          
    Dans l'exemple ci-dessus, la Map renvoyée par asMap contient à la fois la valeur du composant du record name ainsi que la valeur de age définie dans moreAttributes.
    Écrire le code de asMap().
    Vérifier que les tests marqués "Q5" passent.
    Note : dans l'exemple, on peut remarquer que la méthode asMap() est bien définie dans l'interface Expando et pas dans le record Person.
    Rappel : il existe déjà les classes AbstractMap et AbstractSet et la méthode Map.entry(key, value).

  6. Si vous ne vous en êtes pas déjà rendu compte, les méthode get(), getOrDefault() et containsKey() de la Map renvoyée par asMap() ont une complexité affreuse, il faut les réimplanter proprement.
    Implanter les méthodes get(), getOrDefault() et containsKey() de la Map renvoyée par asMap() pour qu'elles aient une complexité raisonnable.
    Vérifier que les tests marqués "Q6" passent.

  7. La méthode forEach de la Map renvoyée par asMap() a une bonne complexité, mais fait des allocations qui ne sont pas nécessaires.
    Ré-implanter la méthode forEach de la Map renvoyée par asMap() pour éviter les allocations d'objets inutiles.
    Vérifier que les tests marqués "Q7" passent toujours.

  8. Le Stream renvoyé par l'appel à la méthode stream sur le résultat de l'appel à la méthode entrySet sur la Map renvoyée par asMap() peut être amélioré, aussi bien en termes de characteristics renvoyées, qu'en termes d'algorithme utilisé pour implanter la version parallèle du Stream.
    Créer votre propre implantation du Stream renvoyée par l'appel à entrySet().stream() sur la Map renvoyée par asMap().
    Vérifier que les tests marqués "Q8" passent.

  9. Enfin, si vous êtes balèze, on aimerait que la Map renvoyée par asMap ait ses couples nom/valeur ordonnés si jamais la Map passée en paramètre d'un Expando en tant que moreAttributes est elle-même ordonnée. Dans ce cas, la Map renvoyée par asMap devra montrer les valeurs définies en tant que composants du record suivis des valeurs stockées dans moreAttributes.
    Vérifier que les tests marqués "Q9" passent.