:: Enseignements :: Master :: M1 :: 2018-2019 :: Java Avancé ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) | 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é.
-
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.
-
Écrire la classe LoggerConf comme décrite ci-dessus.
Vérifier que les tests unitaires marqués "Q2" passent.
-
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.
-
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).
-
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.
-
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.
-
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.
-
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.
© Université de Marne-la-Vallée