:: Enseignements :: ESIPE :: E4INFO :: 2019-2020 :: Java Inside ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) |
For the record ...
|
MethodHandle, StringConcatFactory, JUnit
Le but de cet exercice est d'implanter une méthode createToStringMH
qui permet d'afficher automatiquement des objets record.
Record
La version 14 de Java introduit (sous forme de
preview-feature)
une nouvelle sorte de classe appelée
record.
Un record est une classe qui possède la même API interne et externe,
donc pas d'encapsulation.
Voici comment on déclare un record
Point avec
deux
record components:
x et
y.
record Point(int x, int y) {
}
Lorsque l'on déclare un record (qui est forcément non mutable),
le compilateur ajoute des accesseurs (
x() et
y())
ainsi que les méthodes
equals,
hashCode et
toString.
Par exemple, le code suivant
var point = new Point(1, 2);
System.out.println(point);
System.out.println(point.x());
System.out.println(point.y());
affiche
Point[x=1, y=2] puis
1 puis
2.
Les records possèdent aussi une API de reflection spécifique
avec deux méthodes supplémentaires sur la classe
java.lang.Class
/** Returns true if and only if this class is a record class.
* @return true if and only if this class is a record class
*/
public boolean isRecord()
/** Returns an array containing {@code RecordComponent} objects reflecting all the
* declared record components of the record represented by this {@code Class} object.
* The components are returned in the same order that they are declared in the
* record header.
*
* @return The array of {@code RecordComponent} objects representing all the
* record components of this record. The array is empty if this class
* is not a record, or if this class is a record with no components.
*/
public RecordComponent[] getRecordComponents()
sachant qu'un record component est défini comme cela
package java.lang.reflect;
public final class RecordComponent {
/**
* Returns the name of this record component.
* @return the name of this record component
*/
public String getName() { ... }
/**
* Returns a {@code Class} that identifies the declared type for this
* record component.
* @return a {@code Class} identifying the declared type of the component
* represented by this record component
*/
public Class<?> getType() { ... }
/**
* Returns a {@code Method} that represents the accessor for this record
* component.
* @return a {@code Method} that represents the accessor for this record
* component
*/
public Method getAccessor() { ... }
...
jdk-14-record
Bien sûr, comme les records ne sont pas encore une feature officielle,
il faudra utiliser un JDK particulier que vous pouvez trouver dans
http://igm.univ-mlv.fr/~forax/tmp/exam-java-inside/.
De plus comme vos IDEs préférés ne sont pas encore prêts et que Maven marche mal offline,
voici le script de build que vous utiliserez
echo with JAVA_HOME=$JAVA_HOME
javac=$JAVA_HOME/bin/javac
java=$JAVA_HOME/bin/java
echo compile java files
$javac --source-path src \
-d target \
--enable-preview \
--release 14 \
src/fr/umlv/exam/*.java
exitCode=$?
if [ $exitCode -ne 0 ]; then
exit $exitCode
fi
echo execute main
$java --class-path target \
--enable-preview \
fr.umlv.exam.Main
Ce script est à sauvegarder sous le nom
build.sh à la racine de votre projet.
StringConcatFactory
Depuis la version 9 du JDK, il existe une méthode makeConcatWithConstants dans la classe
java.lang.invoke.StringConcatFactory qui renvoie un MethodHandle permettant d'effectuer la concaténation
d'une chaine de caractère avec des trous.
Comme cette méthode fait partie du JDK habituel, je vous laisse lire la javadoc
(celle-ci doit être disponible en tapant jdoc dans la console d'un terminal).
Exercice 1 - Plein de toString ...
Le but de ce TP noté est d'écrire une méthode createToStringMH
capable pour une classe record de renvoyer un MethodHandle
qui retourne une String d'affichage lorsqu'on l'invoque.
Avant d'écrire cette méthode, nous écrirons deux autres méthodes pour
vous aider à comprendre comment cela marche.
-
Dans le répertoire EXAM, créer avec votre éditeur préféré un projet nommé
exam-tostring avec un répertoire src contenant le package
fr.umlv.exam.
Puis, écrire dans le package fr.umlv.exam une classe Main
avec une méthode main de test.
Puis, recopier le script de build ci-dessus dans un fichier build.sh
Enfin, après avoir configuré la variable d'environement JAVA_HOME
export JAVA_HOME=/path/to/your/jdk-14-record
Exécuter le script de build et vérifier que le main s'exécute correctement
-
On souhaite, dans une classe fr.umlv.exam.ObjectMethodGenerator,
créer une méthode createStringConcatMH qui prend en paramètre un objet Lookup
et renvoie un MethodHandle qui, lorsqu'il est appelé, correspond à la concatenation
suivante "name=" + param1 + ", age=" + param2 avec param1 et param2
les paramètres du MethodHandle.
public class Main {
private static final MethodHandle STRING_CONCAT =
ObjectMethodGenerator.createStringConcatMH(lookup());
public static String stringConcat(String name, int age) throws Throwable {
return (String)STRING_CONCAT.invokeExact(name, age);
}
public static void main(String[] args) throws Throwable {
System.out.println(stringConcat("bob", 21)); // affiche name=bob, age=21
...
Rappel: il existe une méthode dynamicInvoker sur la classe CallSite.
-
Créer une classe de test JUnit 5 ObjectMethodGeneratorTests et écrire un test
vérifiant que le code de createStringConcatMH fonctionne correctement.
Vous utiliserez assertAll pour tester plusieurs appels à la méthode createStringConcatMH.
Note: les tests doivent être exécutés par votre environement de développement mais pas par le script de build.
-
Ecrire dans un fichier Person.java un record composé d'un nom et d'un age.
Et vérifier que le script de build passe et que le code suivant fonctionne
var person = new Person("bob", 21);
System.out.println(person); // Person[name=bob, age=21]
System.out.println(person.name()); // bob
System.out.println(person.age()); // 21
-
On cherche maintenant à écrire dans la classe ObjectMethodGenerator une méthode createToStringFromValuesMH
qui prend en paramètre un object Lookup et une Class d'un record et renvoie un method handle
qui possède autant de paramètre que de record component (le type d'un paramètre est le type du record component correspondant)
et renvoie une chaîne de caractères formatée de la même façon que le toString.
Faire en sorte que le code suivant affiche le même résultat qu'un appel à toString sur une instance de la classe Person.
private static final MethodHandle TO_STRING_FROM_VALUE =
ObjectMethodGenerator.createToStringFromValuesMH(lookup(), Person.class);
public static String toGeneratedStringFromValues(String name, int age) throws Throwable {
return (String)TO_STRING_FROM_VALUE.invokeExact(name, age);
}
public static void main(String[] args) throws Throwable {
var person = new Person("bob", 21);
System.out.println(toGeneratedStringFromValues(person.name(), person.age())); // Person[name=bob, age=21]
-
Toujours dans la classe ObjectMethodGeneratorTests,
vérifier avec un nouveau test que la méthode createToStringFromValuesMH
envoie les bonnes exceptions si les conditions ne sont pas valides.
-
Enfin écrire dans la classe ObjectMethodGenerator une méthode createToStringMH
qui prend en paramètre un object Lookup et une Class d'un record et renvoie un method handle
qui possède un seul paramètre du type du record
et renvoie une chaîne de caractères formatée de la même façon que le toString.
Faire en sorte que le code suivant affiche le même résultat qu'un appel à toString sur une instance de la classe Person.
private static final MethodHandle TO_STRING =
ObjectMethodGenerator.createToStringMH(lookup(), Person.class);
public static String toGeneratedString(Person person) throws Throwable {
return (String)TO_STRING.invokeExact(person);
}
public static void main(String[] args) throws Throwable {
var person = new Person("bob", 21);
System.out.println(toGeneratedString(person)); // Person[name=bob, age=21]
Pour obtenir un method handle à partir d'un objet java.lang.reflect.Method, vous pouvez utiliser Lookup.unreflect().
Pour appeler les bon accessors, l'astuce consiste à utiliser les méthodes MethodHandles.permuteArguments et MethodHandles.filterArguments.
La méthode permuteArguments permet entre autre de dupliquer une instance sur autant de paramètre que l'on veut
(par exemple, passer d'une méthode avec un objet en paramètre à une méthode avec le même objet mais appelé pour chacun des deux paramètres).
La méthode filterArguments permet d'appeler pour chaque paramètre une fonction différente (sous forme d'un tableau pris en paramètre
de filterArguments). Dans notre cas, on veut appeler les accesseurs de chaque record component.
Lire calmement la javadoc de ces méthodes aide (si si).
© Université de Marne-la-Vallée