Publié par

Il y a 4 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 et hashCode

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 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 :
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 :

Publié par

Commentaire

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

  1. Publié par , Il y a 2 mois

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

Laisser un commentaire

Votre adresse de messagerie 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.