Accès rapide :
La vidéo
Introduction aux notions de classes immuables et de records
Mise en oeuvre d'une classe immuable avec une syntaxe traditionnelle
Mise en oeuvre d'une classe immuable via la syntaxe « record »
Ajouter des membres dans un record
Ajouter des constructeurs
Ajouter des méthodes
Les records sont des nouveaux types d'objets immuables en Java. Ce concept est apparu à partir de la version Java SE 16. Cette vidéos vous montre comment définir des records et qu'elles sont les similitudes et différences avec les classes immuables traditionnelles.
Parfois, on peut juger utile de définir des classes immuables (ou immutables). Une telle classe permet de produire des objets avec un état initial mais qui ne pourront plus changer d'état une fois instanciés.
Vous connaissez déjà au moins une classe de ce type. Effectivement, la classe java.lang.String
permet de produire des instances immuables.
Une fois une chaîne instanciée, il n'est plus possible d'en modifier son état et ce jusqu'à sa libération par le Garbage Collector.
De même la classe java.time.LocalDate
, qui permet de gérer une date, produit aussi des instances immuables.
Voici extrait de la JavaDoc officielle - « LocalDate is an immutable date-time object that represents a date, often viewed as year-month-day.
».
Pour produire de tels objets, nous n'avions, avant Java SE 16, qu'une seule et unique manière de procéder. Il fallait produire une classe réalisant une encapsulation de ses membres (attributs privés) et n'exposant que les getters et surtout pas les setters. Un certain volume de code était donc nécessaire pour produire une classe d'instances immuables.
Mais l'arrivée de Java SE 16, change la donne avec l'apparition du concept de « record ». Dans les cas les plus simples, une unique ligne de code peut permettre la définition d'une classe d'objets immuables. Nous allons donc pouvoir faire de grosses économies de lignes de codes.
javac --enable-preview MyApp.java
Afin de comparer les choses, nous allons appréhender les deux approches, en commençant par l'approche historique.
Exemple à considérer une classe Point
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
package fr.koor.samples; import java.util.Objects; public class Point { /* * Deux attributs finaux ne pouvant plus être * modifiés après la construction de l'instance. */ private final double x; private final double y; /* * Un constructeur de classe avec une délégation * à un second constructeur acceptant deux flottants. */ public Point() { this( 0, 0 ); } /* * Un second constructeur permettant de fixer l'état * initial des deux attributs de la classe. */ public Point( double x, double y ) { this.x = x; this.y = y; } /* * Deux getters pour l'accès à l'état de vos points. * Les setters ne sont pas disponibles afin de garantir * l'aspect immuable (readonly) de vos objets. */ public double getX() { return x; } public double getY() { return y; } /* * Cette méthode est utile si vous souhaitez utiliser des instances * de points dans une table associative (type Hashtable ou HashMap). * Elle permettra de calculer le "hashcode" (en gros, la position * dans la collection) d'un point donné. */ @Override public int hashCode() { return Objects.hash(x, y); } /* * Cette méthode permettra de comparer deux instances Java et * d'indiquer si elles sont identiques ou non. */ @Override public boolean equals( Object object ) { if ( this == object ) { return true; } else if ( object instanceof Point ) { Point other = (Point) object; return x == other.x && y == other.y; } else { return false; } } /* * Pour calculer la chaîne de caractères de représentative d'un point. */ @Override public String toString() { return "Point [x=" + x + ", y=" + y + "]"; } // TODO: imaginez d'autres méthodes relatives à la gestion de vos points. } |
Depuis la version 16 du Java SE, le concept de « record » est donc officiellement utilisable : ce concept permet de définir quasiment l'équivalent de la classe présentée ci-dessus en une seule ligne de code, comme le montre l'exemple suivant.
1 |
public record Point (double x, double y) {} |
Il faut comprendre que cette simple définition a produit automatiquement un certain nombre d'éléments dans la classe. Ont automatiquement été définis :
Un constructeur à deux paramètres (par contre, il n'y a pas eut de génération d'un constructeur à zéro paramètre).
Deux attributs x
et y
.
Pour chaque attribut, une méthode d'accès en lecture. Mais attention, ces méthodes ne sont pas préfixées par get
).
Une méthode equals
.
Une méthode hashCode
.
Une méthode toString
.
Pour preuve, voici les résultats produits par l'outil javap invoqué sur le fichier fr/koo/samples/Point.class
qui a été généré par le compilateur.
$> javap fr.koor.samples.Point Compiled from "Point.java" public final class fr.koor.samples.Point extends java.lang.Record { public fr.koor.samples.Point(double, double); public double x(); public double y(); public final java.lang.String toString(); public final int hashCode(); public final boolean equals(java.lang.Object); } $> javap -c fr.koor.samples.Point Compiled from "Point.java" public final class fr.koor.samples.Point extends java.lang.Record { public fr.koor.samples.Point(double, double); Code: 0: aload_0 1: invokespecial #11 // Method java/lang/Record."<init>":()V 4: aload_0 5: dload_1 6: putfield #14 // Field x:D 9: aload_0 10: dload_3 11: putfield #16 // Field y:D 14: return public double x(); Code: 0: aload_0 1: getfield #14 // Field x:D 4: dreturn public double y(); Code: 0: aload_0 1: getfield #16 // Field y:D 4: dreturn public final java.lang.String toString(); Code: 0: aload_0 1: invokedynamic #26, 0 // InvokeDynamic #0:toString:(Lfr/koor/samples/Point;)Ljava/lang/String; 6: areturn public final int hashCode(); Code: 0: aload_0 1: invokedynamic #31, 0 // InvokeDynamic #0:hashCode:(Lfr/koor/samples/Point;)I 6: ireturn public final boolean equals(java.lang.Object); Code: 0: aload_0 1: aload_1 2: invokedynamic #36, 0 // InvokeDynamic #0:equals:(Lfr/koor/samples/Point;Ljava/lang/Object;)Z 7: ireturn } $>
.class
.
L'option -c
permet de demander un désassemblage.
Nous sommes bien d'accord sur le fait que nous ne sommes pas là pour apprendre le langage machine d'un processeur Java :
seules les noms des méthodes produites nous intéresse.
final
.
Dit autrement, vous ne pouvez pas en hériter.
Vous pouvez donc maintenant utiliser votre type « record » quasiment comme s'il s'agissait d'une classe traditionnelle. Vous noterez néanmoins l'absence du préfixe get sur les méthodes d'accès aux attributs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package fr.koor.samples; public class Start { public static void main( String[] args ) { Point p1 = new Point( 0, 0 ); System.out.println( p1 ); Point p2 = new Point( 10, 10 ); System.out.println( p2 ); System.out.println( "Egalité : " + ( p1 == p2 ) ); System.out.println( "Hash code : " + p2.hashCode() ); System.out.println( "Getters : " + p2.x() + " - " + p2.y() ); } } |
Vous pouvez, bien entendu, compléter un record avec vos propres membres (méthodes, constructeurs...).
Afin de proposer plus de facilité pour la construction des instances de votre type record, vous pouvez offrir plusieurs constructeurs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package fr.koor.samples; public record Point(double x, double y) { public Point() { this( 0, 0 ); } public Point( double x, double y ) { this.x = x; this.y = y; } } |
Comme vous le constatez, il est possible de directement utiliser les attributs x
et y
pour donner l'état initial à l'instance.
Testons maintenant notre constructeur à zéro paramètre.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package fr.koor.samples; public class Start { public static void main( String[] args ) { // Test du constructeur sans paramètre. Point p1 = new Point(); System.out.println( p1 ); // Test du constructeur à deux paramètres. Point p2 = new Point( 10, 10 ); System.out.println( p2 ); System.out.println( "Egalité : " + ( p1 == p2 ) ); System.out.println( "Hash code : " + p2.hashCode() ); System.out.println( "Getters : " + p2.x() + " - " + p2.y() ); } } |
Pour ajouter une méthode à votre type record, procédez comme pour une classe traditionnelle. Comme vous le constatez dans la capture d'écran ci-dessous, votre méthode peut accéder directement aux attributs de la classe, mais aussi aux méthodes d'accès en lecture.
Bien entendu, vous n'avez un accès qu'en consultation sur l'état de vos records. Effectivement, aucune méthode d'accès en écriture n'est disponible.
De plus, les attributs x
et y
étant déclarés finaux (mot clé final
) dans la classe, vous ne pourrez pas les
modifier.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package fr.koor.samples; public record Point(double x, double y) { public Point() { this( 0, 0 ); } public Point( double x, double y ) { this.x = x; this.y = y; } public void doSomething() { // Accès aux attributs du record. System.out.printf( "%f,%f with attributes\n", this.x, this.y ); // Accès aux méthodes d'accès du record. System.out.printf( "%f,%f with accessors\n", this.x(), this.y() ); } } |
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :