Il y a 10 mois -
Temps de lecture 6 minutes
Introduction au Record de Java 14
Avertissement
Cette fonctionnalité est disponible en avant-première, ce qui nécessite d’utiliser l’option --enable-preview
et que des évolutions majeures peuvent survenir dans les prochaines versions de Java.
Qu’est-ce que c’est un record ?
the state, the whole state, and nothing but the state
Un record
est une forme restrictive de classe qui à pour but de générer une classe immuable à partir d’une structure simple. On peut définir un record
comme l’enregistrement d’un état.
Anatomie d’un Record
Un record
génère une classe finale qui étend la classe java.lang.Record
(un record peut implémenter autant d’interfaces qu’une classe normale) avec les éléments suivants :
- Un champ privé et final par élément défini dans l’entête du record. Chaque champ reprend le nom défini dans le record.
- Un accesseur public par élément du record, avec comme nom, le nom de l’élément.
- Un constructeur avec tous les éléments du record, avec les noms des éléments comme nom pour les paramètres.
- Les méthodes
toString
,equals
ethashCode
Exemples de code
Voici un exemple d’un record :
public record Pet(String name, int age) {}
Qui devient après compilation :
❯ javap -p Pet Compiled from "Pet.java" public final class Pet extends java.lang.Record { private final java.lang.String name; private final int age; public Pet(java.lang.String, int); public java.lang.String toString(); public final int hashCode(); public final boolean equals(java.lang.Object); public java.lang.String name(); public int age(); }
Il est possible d’ajouter des méthodes (d’instances et statiques), d’ajouter des champs statiques (mais pas de champs d’instance en dehors de l’entête du record), de créer des constructeurs, et de redéfinir toutes les méthodes générées :
public record Pet(String name, int age) { public Pet(String name, int age) { this.name = name; this.age = age; } public Pet(String name) { this.name = name; this.age = 0; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Pet pet = (Pet) o; return age == pet.age && Objects.equals(name, pet.name); } @Override public int hashCode() { return Objects.hash(name, age); } @Override public String toString() { return new StringJoiner(", ", Pet.class.getSimpleName() + "[", "]") .add("name='" + name + "'") .add("age=" + age) .toString(); } public String name() { return name; } public int age() { return age; } // Exemple de méthode rajoutée dans le record public Pet withAge(int age) { return new Pet(this.name, age); } }
Avec l’ajout des records, un mécanisme de constructeur simplifié a été ajouté. Ce mécanisme permet de compléter le constructeur généré par le compilateur à partir de la définition du record :
public record Pet(String name, int age) { public Pet { if (age < 0) { throw new IllegalArgumentException("Age cannot be negative"); } if (name == null || name.isBlank()) { throw new IllegalArgumentException("Name cannot be blank"); } } }
Ce qui donne une fois compilée :
javap -c Pet Compiled from "Pet.java" public final class Pet extends java.lang.Record { public Pet(java.lang.String, int); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Record."<init>":()V 4: iload_2 5: ifge 18 8: new #7 // class java/lang/IllegalArgumentException 11: dup 12: ldc #9 // String Age cannot be negative 14: invokespecial #11 // Method java/lang/IllegalArgumentException."<init>":(Ljava/lang/String;)V 17: athrow 18: aload_1 19: ifnull 29 22: aload_1 23: invokevirtual #14 // Method java/lang/String.isBlank:()Z 26: ifeq 39 29: new #7 // class java/lang/IllegalArgumentException 32: dup 33: ldc #20 // String Name cannot be blank 35: invokespecial #11 // Method java/lang/IllegalArgumentException."<init>":(Ljava/lang/String;)V 38: athrow 39: aload_0 40: aload_1 41: putfield #22 // Field name:Ljava/lang/String; 44: aload_0 45: iload_2 46: putfield #28 // Field age:I 49: return [...] }
On voit bien le code rajouté avant la ligne 39.
Comparaison avec Kotlin & Scala
Reprenons l’exemple du record Pet définit plus tôt :
record Pet(String name, int age) {}
Maintenant voyons ce que ça donnerait avec des solutions équivalentes en Kotlin :
data class Pet(val name: String,val age: Int)
Et Scala :
case class Pet(name: String, age: Int)
Nous constatons que nous nous trouvons avec des formes similaires entre les 3 langages mais, nous avons les différences suivantes :
- En Kotlin, les
data class
peuvent générer des classes mutables (en ajoutant des champsvar
au lieu de champsval
). - Kotlin et Scala génèrent en plus une méthode
copy
(qui permet de copier les instances en replaçant certaines valeurs). Rien ne semble prévu du côté desrecord
pour avoir un mécanisme similaire. - Les
data class
et lescase class
intègrent des mécanismes de déconstruction (récupérer les valeurs des membres). Une version limitée est prévue dans lesrecord
dans des futures versions de Java (et plus particulières avec des évolutions du pattern matching prévues ultérieurement). - Du fait qu’un
record
est avant tout une classe en Java, les accolades restent nécessaires. - La plus grande différence entre ces 3 implémentations vient du code JVM généré. Je vous renvoie vers un article d’Ali Dehghani : Java Records: A Closer Look, qui va beaucoup plus loin à ce sujet.
Cas d’utilisation & Conclusion
Maintenant que nous avons vu comment créer des records, voyons une petite liste non exhaustive de cas d’utilisation :
- En remplacement de beaucoup de cas d’utilisation Java Bean, voir supprimer (du moins limiter) l’usage de framework du type Lombok ou Immutables.
- En tant que POJO exploité dans des frameworks tel que Jackson ou Hibernate.
- En tant que type intermédiaire lors d’opération sur les streams :
List<Person> topThreePeople(List<Person> list) { // On peut avoir un record local! record PersonX(Person p, int hash) { PersonX(Person p) { this(p, p.name().toUpperCase().hashCode()); } } return list.stream() .map(PersonX::new) .sorted(Comparator.comparingInt(PersonX::hash)) .limit(3) .map(PersonX::person) .collect(toList()); }
- Pour faire un retour multi valeur ou pour générer des clés composites pour les maps :
//Exemple de retour multi valeur record MinMax(int min, int max) {}; public MinMax minmax(int[] elements) { ... } //Exemple de clé de map composite record PersonPlace(Person person, Place place) { }; Map<PersonPlace, LocalDateTime> lastSeen = ... ... LocalDateTime date = lastSeen.get(new PersonPlace(person, place)); ...
Pour conclure, nous avons fait un rapide tour des records, un nouveau mécanisme qui permet de réduire le boilerplate code lors de la création de classes immuables avec une écriture concise et apporter un peu de modernité au langage Java. Mais, les records se limitent juste à la génération de classes immuables avec une convention de nommage différente des Java Bean.
Références
Voici quelques liens en tant que référence et pour aller plus loin sur le sujet :
Commentaire
1 réponses pour " Introduction au Record de Java 14 "
Published by Julien SMADJA , Il y a 8 mois
Merci pour cet article ! J’espère en voir d’autres sur Java 14.