Published by

Il y a 2 ans -

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 et hashCode

Exemples de code

Voici un exemple d’un record :

[java]public record Pet(String name, int age) {}[/java]

Qui devient après compilation :

[java]❯ 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();
}
[/java]

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 :

[java]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);
}
}
[/java]

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 :

[java]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");
}
}
}
[/java]

Ce qui donne une fois compilée :

[java]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

[…]
}
[/java]

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 :

[java]record Pet(String name, int age) {}[/java]

Maintenant voyons ce que ça donnerait avec des solutions équivalentes en Kotlin :

[java]data class Pet(val name: String,val age: Int)[/java]

Et Scala :

[scala]case class Pet(name: String, age: Int)[/scala]

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 champs var au lieu de champs val).
  • 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é des record pour avoir un mécanisme similaire.
  • Les data class et les case class intègrent des mécanismes de déconstruction (récupérer les valeurs des membres). Une version limitée est prévue dans les record 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 :

[java]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());
}[/java]

  • Pour faire un retour multi valeur ou pour générer des clés composites pour les maps :

[java]//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));
…[/java]

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 :

Published by

Commentaire

1 réponses pour " Introduction au Record de Java 14 "

  1. Published by , Il y a 2 ans

    Merci pour cet article ! J’espère en voir d’autres sur Java 14.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Sapient, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.