:: Enseignements :: ESIPE :: E4INFO :: 2019-2020 :: Java Inside ::
[LOGO]

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.

  1. 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
  2. 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.
  3. 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.
  4. 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
        
  5. 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]
        
  6. 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.
  7. 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).