:: Enseignements :: Master :: M1 :: 2018-2019 :: Java Avancé ::
[LOGO]

Examen de Java Avancé - Session 2


Le but de ce TP est d'implanter un ensemble de méthodes génériques toString, toExtendedString, generateAll et generateAllAsStream permettant d'afficher et de générer des configurations (voir ci-dessous).

Vos sources Java produites pendant l'examen devront être placées sous le répertoire EXAM de votre compte ($HOME) (qui est vierge dans l'environnement de TP noté); sinon, elles ne seront pas récupérées.

Tout document papier est proscrit.
Vous pouvez consulter la javadoc à travers Eclipse (onglet Javadoc) ou en tapant jdoc dans un terminal. Sinon la doc est là: /usr/local/apps/java11/docs/api/index.html.
Les seuls documents électroniques autorisés sont les supports de cours à l'url http://igm.univ-mlv.fr/~forax/ens/java-avance/cours/pdf/.

Exercice 1 - Configurations

Une configuration est un objet qui ne contient que des propriétés (des champs) qui peuvent être initialisées ou non et les getters/setters correspondants. Cela permet d'initialiser des objets complexes en utilisant une configuration comme paramètre de leur constructeur.
Par exemple, un logger est un objet qui a (traditionnellement) un nom et un niveau de log. La classe de l'objet configuration correspondant est LoggerConf : elle va contenir deux champs name et level ainsi que des getters et setters et une méthode toString.
Ainsi, le constructeur de logger prendra en paramètre un objet de la classe LoggerConf pour initialiser le logger.
Comme une propriété peut être initialisée ou non, les getters utilisent java.util.Optional comme type de retour. Si l'Optional est vide (isEmpty), cela veut dire que la propriété n'est pas initialisée. De plus, les setters peuvent être chaînés (comme avec un builder classique) et ils n'acceptent pas null comme valeur possible.
Comme le contrat des getters/setters diffère légèrement du contrat habituel, au lieu d'utiliser des méthodes préfixées par get et set, nous utiliserons la convention suivante.
  • Une classe de configuration a un constructeur sans argument qui produit une configuration vide.
  • Un getter est une méthode qui a le même nom que le champ et renvoie un Optional.
  • Un setter est une méthode qui a le même nom que le champ et prend en paramètre la valeur pour initialiser la propriété. Le type de retour est celui de la classe pour pouvoir chaîner les appels.
  • Une méthode toString qui affiche les valeurs des propriétés séparées par des virgules et encadrées par des accolades. Si une propriété n'est pas initialisée, alors sa valeur ne doit pas être affichée. L'ordre d'affichage des propriétés doit toujours être le même, indépendamment de l'ordre d'initialisation de celles-ci.

Voilà un exemple d'utilisation de la classe LoggerConf.
  var conf = new LoggerConf();
  var name = conf.name();             // renvoie un Optional vide
  System.out.println(name.isEmpty()); // true
  conf.name("hello");                 // la propriété name est initialisée à "hello"
  System.out.println(conf.name().orElseThrow());  // hello
    

De plus, comme les setters peuvent être chaînés, il est possible de créer et initialiser le nom d'un LoggerConf en une seule expression.
  var conf = new LoggerConf().name("hello");
  System.out.println(conf.name().orElseThrow());  // hello
    

Certains tests unitaires correspondant à l'implantation sont ici: ConfTest.java.
Remarque : certains tests qui prennent null en argument lèvent un warning ; c'est normal. Pour retirer le warning, il faut indiquer le type du paramètre, mais comme trouver le type des paramètres fait partie de l'exercice, j'ai laissé les warnings.

Tout warning ou présence de @SuppressWarnings dans votre code sera sévèrement sanctionné.

  1. Avant d'écrire une classe de configuration, nous avons besoin d'une classe auxiliaire.
    Écrire une énumération LogLevel dans le package fr.umlv.conf contenant les valeurs INFO, WARNING et ERROR de telle façon que les tests unitaires marqués "Q1" passent.
  2. Écrire la classe LoggerConf comme décrite ci-dessus.
    Vérifier que les tests unitaires marqués "Q2" passent.
  3. Ajouter une méthode toString à la classe LoggerConf en respectant les consignes indiquées ci-dessus et sachant que la propriété name doit être affichée avant la propriété level.
    Vérifier que les tests unitaires marqués "Q3" passent.
  4. On peut remarquer que, comme la classe LoggerConf expose ses getters de façon publique, on peut écrire une méthode toString (dans une classe ConfHelper) prenant en paramètre une instance de LoggerConf puis, séparés par des virgules, la liste des getters des propriétés que l'on souhaite afficher (l'ordre des getters indiquant l'ordre d'affichage).
    Dans ce cas, la méthode toString de la classe LoggerConf peut être ré-écrite ainsi :
            return ConfHelper.toString(this, LoggerConf::name, LoggerConf::level);
          

    Écrire la méthode ConfHelper.toString, en utilisant un Stream pour l'implantation, et vérifier que les tests unitaires marqués "Q4" passent.
    Note : si vous n'arrivez pas à utiliser un Stream, écrivez une version sans ; mais vous ne devez absolument pas allouer de collection (le code sera déjà assez compliqué comme cela).
  5. On peut constater que l'on peut écrire la méthode ConfHelper.toString de façon indépendante de la classe LoggerConf dans le but d'utiliser toString avec n'importe quelle classe qui expose ses getters.
    Changer la signature de la méthode toString pour la rendre plus générique et vérifier que les tests unitaires marqués "Q5" passent.
  6. Le problème de la méthode toString écrite précédemment est qu'elle ne permet pas de distinguer, à l'affichage, deux propriétés qui auraient la même valeur (puisque l'on affiche uniquement les valeurs). On se propose de corriger ce problème en ajoutant une méthode toExtendedString dans la classe ConfHelper.
    Écrire la méthode toExtendedString qui prend en paramètre une configuration et un ensemble de couples nom de propriété/getter de la propriété séparés par des virgules. (Les couples sont des Map.Entry que l'on peut construire facilement avec la méthode Map.entry()). La méthode toExtendedString méthode affiche les couples avec un ":" entre chaque clé et sa valeur ; les couples sont séparés par des virgules, le tout encadré par des accolades (un peu comme en JSON, mais sans les "" pour les Strings).
    Vérifier que les tests unitaires marqués "Q6" passent.
    Note :comme pour la question précédente, si vous n'arrivez pas à utiliser un Stream, écrivez une version sans. Vous ne devez en aucun cas allouer de collection.
  7. On peut aussi remarquer que, comme une configuration expose ses setters, il est possible de générer toutes les configurations possibles pour une classe. On se propose d'écrire une méthode generateAll dans la classe ConfHelper. Elle prend en paramètre une fonction qui permet de créer une configuration ainsi qu'une liste de lambdas séparées par des virgules, chacune appelant un setter avec une valeur en paramètre et qui renvoie un ensemble de toutes les configurations qu'il est possible d'obtenir en appelant ou non les lambdas, dans l'ordre dans lequel elles sont données en paramètre.
    Par exemple:
           generateAll(LoggerConf::new, c -> c.name("jay"), c -> c.level(LogLevel.WARNING))
          
    va générer les configurations correspondant à {}, {WARNING}, {jay}, {jay, WARNING}.
    En effet, le premier setter peut être appelé ou non, donc on obtient {} et {jay}. Puis, sur ces configurations, le second setter peut être appelé ou non, donc pour {} on obtient les configurations {} et {WARNING} et pour {jay} on obtient les configuration {jay} et {jay, WARNING}.
    Écrire la méthode generateAll dans la classe ConfHelper.
    Vérifier que les tests unitaires marqués "Q7" passent.
  8. On peut remarquer qu'il n'est pas nécessaire d'allouer l'ensemble qui sert de valeur de retour mais que l'on peut concaténer des Stream à la place.
    Écrire la méthode generateAllAsStream dans la classe ConfHelper qui prend les mêmes paramètres que la méthode generateAll et génère l'ensemble des configurations sous forme d'un Stream.
    Vérifier que les tests unitaires marqués "Q8" passent.