Jouons avec les Records Java

Avec la sortie de Java 16, les records ne sont plus en preview, il est donc tout à fait envisageable de les utiliser !

Prenons le temps d'un article pour éprouver un peu ce nouveau jouet.

Késako

Les records viennent compléter la boîte à outils des "structures" Java (avec les classes, les interfaces, les enums, ...). Pour un record, comme au sirop, "C’qui compte, c’est les valeurs !" - Franck (Perceval) Pitiot.

Les records sont définis par leurs valeurs, ils sont donc immuables (avec tous les bienfaits de ce pattern). Il a toujours été possible de coder des classes immuables et définies par leurs valeurs mais il faut bien admettre que ce n'était pas gratuit. Maintenant, on peut faire :

public record Title(Level level, Label label) {
}

Nous donnera un Title défini par un Level et un Label.

Comme un record est défini par ses valeurs, on a automatiquement les methods equals, hashCode et toString qui sont implémentées en se basant sur ces valeurs. Cela implique aussi qu'on ne peut pas avoir d'autres attributs que ceux définis dans la déclaration.

On a aussi des accesseurs en lecture aux deux attributs avec level() et label() et non pas getLevel() et getLabel() ce qui est peut-être la première pierre d'un changement de style dans Java.

Comme pour les classes, on peut définir des records privés (dans le même fichier que la structure qui en a besoin) et locaux :

public void buildSummary() {
  record Title(Level level, Label label) {}

  // Some stuff using title
}


Voilà pour le résumé très rapide, voyons maintenant ce que l’on peut faire avec !

Les constructeurs des records

Les records ont des constructeurs qui sont générés en fonction de leurs attributs. Pour Title, on a déjà ce constructeur dans le parent :

public record Title(Level level, Label label) {

  public Title(Level level, Label label) {
    this.level = level;
    this.label = label;
  }
}

Il peut être surchargé en respectant les règles classiques de surcharge (on ne pourra pas baisser la visibilité de cette signature, par exemple). Il faut bien garder en tête que les attributs d'un record sont final : on doit donc tous les affecter dans le constructeur, sans quoi, le code ne compilera pas.

Il est possible d'ajouter des traitements et de la logique dans un constructeur avec une syntaxe un peu allégée :

public record Title(Level level, Label label) {

  public Title {
    Assert.notNull("level", level);
    Assert.notNull("label", label);
  }
}

Dans ce cas, on a un call implicite au constructeur avec tous les paramètres.

Il est aussi possible de créer de nouveau constructeurs invoquant le constructeur par défaut :

public record Title(Level level, Label label) {

  public Title() {
    this(Level.ONE, new Label("Title"));
  }
}

Cette dernière option ouvre la porte à l'utilisation de builders ou à tout autre pattern de construction.

Il n'est cependant pas possible de masquer le constructeur par défaut !

Les records avec un seul attribut

J'ai pour habitude de faire des "types" en Java en définissant des classes avec un seul attribut et, parfois, une logique propre à cet attribut. Ces "types" seront maintenant bien plus simples à créer :

public record Label(String label) {
  public Label(String label) {
    Assert.notBlank("label", label);

    this.label = label;
  }

  public String get() {
    return label();
  }
}

Dans ce cas, je pense que la définition d'une method get() peut être une bonne idée pour éviter des invocations du type label.label() (je trouve que label.get() est plus élégant).

Dans certains cas, il peut être intéressant d'ajouter des static factory dans nos records. Ici, on peut imaginer construire un Label depuis un titre dans un markdown :

public record Label(String label) {

  public Label(String label) {
    Assert.notBlank("label", label);

    this.label = label;
  }

  public static Label fromTitle(String title) {
    Assert.notNull("title", title);

    return new Label(title.replaceAll("^#+\\s*(.+)$", "$1"));
  }
}

Les records sont immuables. Modifier un record c'est en créer un nouveau. Il peut donc être intéressant d'avoir ce type de methods :

public Label toUpperCase() {
  return new Label(label().toUpperCase());
}

Je pense que je vais faire des quantités non négligeables de records sur ce modèle dans mes projets tant je trouve l'approche élégante et pratique ! Cette capacité à créer des "types" beaucoup plus simplement va me faire gagner pas mal de temps.

Les records avec plusieurs attributs

Il est possible de définir des records avec plusieurs attributs. Je pense qu'il faut quand même faire attention à ne pas tomber dans l'excès, car leur déclaration peut être rapidement illisible.

Comme pour les classes, s’il y a moins de 3 attributs dans notre record un constructeur prenant en paramètre ces attributs sera utilisable. Au-delà, il faudra passer par des patterns de construction plus riches qui évitent les erreurs et masquent cette complexité d'instanciation.

Dans tous les cas, pouvoir associer des attributs dans des objets naturellement immuables et définis par leurs valeurs va ajouter du confort au quotidien.

Un game changer ?

Je travaille majoritairement sur des applications mettant le Métier au centre dans des architectures hexagonales. Dans mon quotidien, les records vont donc m'éviter des quantités non négligeables de code à faible apport Métier.

Ils ne vont cependant pas changer en profondeur nos applications puisqu'ils n'apportent rien qui n'était pas déjà faisable. Ils sont un très bel ajout syntaxique, un nouvel outil pratique et un pas vers le pattern matching !