W3docs

Les Records Java en profondeur

Plongée approfondie dans les records Java : constructeurs canoniques et compacts, validation et cas d'usage.

Un record est la façon qu'a Java de déclarer une classe dont le seul rôle est de transporter des données. Introduit en aperçu dans Java 14 et finalisé dans Java 16, un record réduit à néant le code répétitif habituel — champs private final, constructeur, accesseurs, equals, hashCode et toString — en une seule ligne d'en-tête. Le chapitre précédent sur les records présentait la syntaxe de base ; celui-ci explore plus en profondeur le comportement réel des records : leurs constructeurs canoniques et compacts, la façon dont ils imposent des invariants, les garanties d'immutabilité offertes, et les contextes où ils conviennent (et ceux où ils ne conviennent pas).

Ce que le compilateur génère pour vous

Lorsque vous écrivez record Point(int x, int y) {}, le compilateur émet une classe final avec deux champs private final, un constructeur public prenant les deux, des méthodes d'accès publiques portant exactement le nom des composants (x(), y() — sans préfixe get), ainsi que des méthodes equals, hashCode et toString basées sur les valeurs.

record Point(int x, int y) {}

// Equivalent to (roughly) hand-writing:
// final class Point {
//   private final int x;
//   private final int y;
//   Point(int x, int y) { this.x = x; this.y = y; }
//   int x() { return x; }
//   int y() { return y; }
//   public boolean equals(Object o) { ... compares x and y ... }
//   public int hashCode() { ... derived from x and y ... }
//   public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
// }

Les x et y dans l'en-tête sont les composants du record. Les membres générés par le compilateur en sont entièrement dérivés, dans l'ordre de déclaration.

Constructeurs canoniques et compacts

Chaque record possède un constructeur canonique dont les paramètres correspondent aux composants. Il est rarement écrit en entier — on utilise plutôt le constructeur compact, qui omet la liste de paramètres et les affectations finales this.field = field. Le compilateur exécute votre code en premier, puis affecte les paramètres (éventuellement modifiés) aux champs. C'est l'endroit naturel pour la validation et la normalisation.

record Range(int low, int high) {
  Range {                                  // compact constructor — no (int low, int high)
    if (low > high) {
      throw new IllegalArgumentException("low must be <= high");
    }
    low = Math.max(low, 0);                // reassigning the parameter normalizes the field
  }
}

Si vous avez besoin de la forme canonique explicite (par exemple, pour copier défensivement un composant mutable), écrivez la signature complète et effectuez vous-même les affectations :

record Tags(String name, List<String> values) {
  Tags(String name, List<String> values) {           // explicit canonical constructor
    this.name = name;
    this.values = List.copyOf(values);               // defensive, unmodifiable copy
  }
}

Immutabilité et ce que les records ne sont pas

Les champs d'un record sont final, de sorte que la référence que chaque composant contient ne change jamais après la construction. Cela rend les records superficiellement immuables. Mais l'immutabilité s'arrête à la référence : si un composant pointe vers un objet mutable (comme un ArrayList), les appelants qui partagent cet objet peuvent toujours en modifier le contenu. Les copies défensives dans le constructeur canonique comblent cette lacune.

PropriétéRecordsClasses ordinaires
Champstoujours private finalau choix
Classeimplicitement finalextensible sauf si final
Superclassetoujours java.lang.Recordn'importe laquelle (par défaut Object)
Accesseursgénérés automatiquement, sans préfixe getécrits manuellement
equals/hashCodebasés sur les valeurs, généréspar identité par défaut
Mutateursaucun — immuableautorisés

Comme un record étend toujours java.lang.Record, il ne peut pas étendre une autre classe. Il peut néanmoins implémenter des interfaces, déclarer des membres statiques et ajouter des méthodes d'instance.

Ajouter du comportement, des membres statiques et des fabriques

Un record est toujours une classe. Vous pouvez lui ajouter des méthodes supplémentaires, des méthodes de fabrique statiques, des champs statiques et même des types imbriqués. Les composants définissent l'état ; tout le reste est du Java ordinaire.

record Money(String currency, long cents) {
  static Money of(String currency, long cents) {     // static factory
    return new Money(currency, cents);
  }
  Money plus(Money other) {                          // derived behavior
    if (!currency.equals(other.currency)) {
      throw new IllegalArgumentException("currency mismatch");
    }
    return new Money(currency, cents + other.cents); // returns a new value
  }
}

Les records s'associent également naturellement aux types scellés et au pattern matching, modélisant des ensembles fermés de formes de données — l'ossature de la conception de données de style algébrique en Java moderne. Une interface scellée fixe l'ensemble des implémentations de records autorisées, et un switch sur ces records peut déconstruire chacun d'eux par ses composants en une seule expression.

Un exemple complet : les records de bout en bout

Ce programme exploite les membres générés d'un record, prouve les propriétés d'immutabilité et de classe via la réflexion, impose un invariant dans un constructeur compact, liste les composants du record dans l'ordre de déclaration, et montre les records fonctionnant avec des collections et un comportement ajouté.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • Le Point pour lequel vous n'avez jamais écrit de corps a quand même affiché Point[x=3, y=4], répondu à a.x(), et indiqué equals by value: true avec des codes de hachage identiques — le compilateur a généré toString, les accesseurs, equals et hashCode basés sur les valeurs à partir des deux seuls composants.
  • La réflexion a confirmé le contrat garanti par le langage : is final class : true (les records ne peuvent pas être sous-classés) et is a record : true (chaque record étend java.lang.Record), ce qui explique l'absence de mutateurs et l'immutabilité des champs.
  • L'appel Range(9, 2) a été rejeté avec low must be <= high. Le constructeur compact s'est exécuté avant l'affectation des champs, de sorte qu'un record n'est jamais construit dans un état invalide — la validation y a sa place, et non dans une vérification de fabrique séparée.
  • getRecordComponents() a retourné les composants dans l'ordre de déclaration sous la forme low:int high:int, montrant que la structure d'un record est introspectable par réflexion — base sur laquelle les bibliothèques de sérialisation et les frameworks mappent automatiquement les records.
  • Money.of("USD", 500).plus(Money.of("USD", 250)) a produit USD 750, et distinct() a fusionné deux valeurs Point(0,0) identiques pour n'en laisser que 2 — les records se comportent comme de véritables valeurs partout, y compris dans les flux et les ensembles, précisément parce que leur equals/hashCode compare les contenus.

Quand utiliser un record (et quand ne pas le faire)

Optez pour un record lorsque le type est défini par ses données et que ces données ne changent pas après la construction :

  • DTOs et charges utiles de requêtes/réponses API.
  • Clés de Map et éléments de Set (equals/hashCode basés sur les valeurs fournis gratuitement).
  • Types de retour regroupant plusieurs valeurs, remplaçant les tuples jetables ou les paramètres de sortie.
  • Les « feuilles » d'une hiérarchie scellée que vous déconstruisez avec le pattern matching.

Préférez une classe ordinaire lorsque :

  • L'objet a un état mutable ou un cycle de vie (entités, constructeurs, services).
  • Vous devez étendre une autre classe — les records ne peuvent qu'implémenter des interfaces.
  • L'identité de l'objet importe plus que son contenu (vous souhaitez l'égalité référentielle).

Un piège courant : l'accesseur d'un record retourne la référence stockée telle quelle. Si un composant est d'un type mutable (un List, un tableau, une Date), copiez-le défensivement dans le constructeur canonique — comme le fait l'exemple Tags ci-dessus avec List.copyOf — sinon les appelants peuvent muter l'état « immuable » du record via la référence qu'ils ont passée.

Entraînement

Pratique
Que permet le constructeur compact d'un record (par exemple 'Range { ... }') que nécessiterait davantage de code dans un corps de constructeur explicite ordinaire ?
Que permet le constructeur compact d'un record (par exemple 'Range { ... }') que nécessiterait davantage de code dans un corps de constructeur explicite ordinaire ?
Was this page helpful?