Explorer les enregistrements Java 14

La dernière fois que nous avons testé l'opérateur instanceof amélioré, qui apparaîtra dans la prochaine 14e version de Java (qui sortira en mars 2020). Aujourd'hui, je voudrais étudier en détail la deuxième fonctionnalité syntaxique, qui apparaîtra également dans Java 14: les enregistrements.

Les enregistrements ont leur propre JEP , mais il ne montre pas beaucoup de détails, il y a donc beaucoup à essayer et à vérifier par vous-même. Oui, vous pouvez bien sûr ouvrir la spécification Java SE, mais il me semble qu'il est beaucoup plus intéressant de commencer à écrire du code vous-même et de regarder le comportement du compilateur dans certaines situations. Alors faites une mouette et asseyez-vous confortablement. Allons-y.

Contrairement à la dernière fois, quand j'ai dû créer une branche JDK spéciale pour les tests d' instanceof , tout cela est déjà dans la branche principale et est disponible dans la première version JDK 14 , que j'ai téléchargée.

Tout d'abord, nous implémentons l'exemple classique avec Point et le compilons:

 record Point(float x, float y) { } 

 > javac --enable-preview --release 14 Point.java Note: Point.java uses preview language features. Note: Recompile with -Xlint:preview for details. 

javac a compilé avec succès le fichier Point.class . Démontons-le et voyons ce que le compilateur a généré pour nous là-bas:

 > javap -private Point.class Compiled from "Point.java" final class Point extends java.lang.Record { private final float x; private final float y; public Point(float, float); public java.lang.String toString(); public final int hashCode(); public final boolean equals(java.lang.Object); public float x(); public float y(); } 

Oui, le compilateur a créé ce qui suit:

  • La classe finale héritée de java.lang.Record (similaire à enum , qui hérite de java.lang.Enum ).
  • Champs finaux privés x et y .
  • Un constructeur public qui correspond à la signature de l'enregistrement lui-même. Un tel constructeur est appelé canonique .
  • Implémentations de toString() , hashCode() et equals() . Fait intéressant, hashCode() et equals() sont final , et toString() ne l'est pas. Il est peu probable que cela affecte quoi que ce soit, puisque la classe elle-même est final , mais quelqu'un sait-il pourquoi ils ont fait cela? (Je ne suis pas)
  • Méthodes de lecture des champs.

Avec le constructeur et les méthodes de lecture, tout est clair, mais je me demande comment exactement toString() , hashCode() et equals() implémentés? Voyons voir. Pour ce faire, exécutez javap avec l'indicateur -verbose :

Goupille de démontage longue
 > javap -private -verbose Point.class Classfile Point.class Last modified 29 . 2019 .; size 1157 bytes SHA-256 checksum 24fe5489a6a01a7232f45bd7739a961c30d7f6e24400a3e3df2ec026cc94c0eb Compiled from "Point.java" final class Point extends java.lang.Record minor version: 65535 major version: 58 flags: (0x0030) ACC_FINAL, ACC_SUPER this_class: #8 // Point super_class: #2 // java/lang/Record interfaces: 0, fields: 2, methods: 6, attributes: 4 Constant pool: #1 = Methodref #2.#3 // java/lang/Record."<init>":()V #2 = Class #4 // java/lang/Record #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Record #5 = Utf8 <init> #6 = Utf8 ()V #7 = Fieldref #8.#9 // Point.x:F #8 = Class #10 // Point #9 = NameAndType #11:#12 // x:F #10 = Utf8 Point #11 = Utf8 x #12 = Utf8 F #13 = Fieldref #8.#14 // Point.y:F #14 = NameAndType #15:#12 // y:F #15 = Utf8 y #16 = Fieldref #8.#9 // Point.x:F #17 = Fieldref #8.#14 // Point.y:F #18 = InvokeDynamic #0:#19 // #0:toString:(LPoint;)Ljava/lang/String; #19 = NameAndType #20:#21 // toString:(LPoint;)Ljava/lang/String; #20 = Utf8 toString #21 = Utf8 (LPoint;)Ljava/lang/String; #22 = InvokeDynamic #0:#23 // #0:hashCode:(LPoint;)I #23 = NameAndType #24:#25 // hashCode:(LPoint;)I #24 = Utf8 hashCode #25 = Utf8 (LPoint;)I #26 = InvokeDynamic #0:#27 // #0:equals:(LPoint;Ljava/lang/Object;)Z #27 = NameAndType #28:#29 // equals:(LPoint;Ljava/lang/Object;)Z #28 = Utf8 equals #29 = Utf8 (LPoint;Ljava/lang/Object;)Z #30 = Utf8 (FF)V #31 = Utf8 Code #32 = Utf8 LineNumberTable #33 = Utf8 MethodParameters #34 = Utf8 ()Ljava/lang/String; #35 = Utf8 ()I #36 = Utf8 (Ljava/lang/Object;)Z #37 = Utf8 ()F #38 = Utf8 SourceFile #39 = Utf8 Point.java #40 = Utf8 Record #41 = Utf8 BootstrapMethods #42 = MethodHandle 6:#43 // REF_invokeStatic java/lang/runtime/ObjectMethods.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object; #43 = Methodref #44.#45 // java/lang/runtime/ObjectMethods.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object; #44 = Class #46 // java/lang/runtime/ObjectMethods #45 = NameAndType #47:#48 // bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object; #46 = Utf8 java/lang/runtime/ObjectMethods #47 = Utf8 bootstrap #48 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object; #49 = String #50 // x;y #50 = Utf8 x;y #51 = MethodHandle 1:#7 // REF_getField Point.x:F #52 = MethodHandle 1:#13 // REF_getField Point.y:F #53 = Utf8 InnerClasses #54 = Class #55 // java/lang/invoke/MethodHandles$Lookup #55 = Utf8 java/lang/invoke/MethodHandles$Lookup #56 = Class #57 // java/lang/invoke/MethodHandles #57 = Utf8 java/lang/invoke/MethodHandles #58 = Utf8 Lookup { private final float x; descriptor: F flags: (0x0012) ACC_PRIVATE, ACC_FINAL private final float y; descriptor: F flags: (0x0012) ACC_PRIVATE, ACC_FINAL public Point(float, float); descriptor: (FF)V flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=3, args_size=3 0: aload_0 1: invokespecial #1 // Method java/lang/Record."<init>":()V 4: aload_0 5: fload_1 6: putfield #7 // Field x:F 9: aload_0 10: fload_2 11: putfield #13 // Field y:F 14: return LineNumberTable: line 1: 0 MethodParameters: Name Flags x y public java.lang.String toString(); descriptor: ()Ljava/lang/String; flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokedynamic #18, 0 // InvokeDynamic #0:toString:(LPoint;)Ljava/lang/String; 6: areturn LineNumberTable: line 1: 0 public final int hashCode(); descriptor: ()I flags: (0x0011) ACC_PUBLIC, ACC_FINAL Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokedynamic #22, 0 // InvokeDynamic #0:hashCode:(LPoint;)I 6: ireturn LineNumberTable: line 1: 0 public final boolean equals(java.lang.Object); descriptor: (Ljava/lang/Object;)Z flags: (0x0011) ACC_PUBLIC, ACC_FINAL Code: stack=2, locals=2, args_size=2 0: aload_0 1: aload_1 2: invokedynamic #26, 0 // InvokeDynamic #0:equals:(LPoint;Ljava/lang/Object;)Z 7: ireturn LineNumberTable: line 1: 0 public float x(); descriptor: ()F flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #16 // Field x:F 4: freturn LineNumberTable: line 1: 0 public float y(); descriptor: ()F flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #17 // Field y:F 4: freturn LineNumberTable: line 1: 0 } SourceFile: "Point.java" Record: float x; descriptor: F float y; descriptor: F BootstrapMethods: 0: #42 REF_invokeStatic java/lang/runtime/ObjectMethods.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object; Method arguments: #8 Point #49 x;y #51 REF_getField Point.x:F #52 REF_getField Point.y:F InnerClasses: public static final #58= #54 of #56; // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles 

Dans l'implémentation de toString() , hashCode() et equals() nous voyons invokedynamic . Cela signifie que la logique de ces méthodes sera générée paresseusement par la machine virtuelle elle-même. Je ne suis pas un grand spécialiste de l'exécution, mais je pense que cela est fait pour une meilleure efficacité. Par exemple, si à l'avenir ils proposent un hachage plus rapide, alors dans cette approche, l'ancien code compilé obtiendra tous les avantages de la nouvelle version. Il réduit également la taille des fichiers de classe.

Mais quelque chose que nous sommes allés trop profondément. Revenons à nos registres de moutons . Essayons d'instancier un Point et voyons comment fonctionnent les méthodes. À partir de maintenant, je n'utiliserai plus javac et exécuterai simplement le fichier java directement:

 public class Main { public static void main(String[] args) { var point = new Point(1, 2); System.out.println(point); System.out.println("hashCode = " + point.hashCode()); System.out.println("hashCode2 = " + Objects.hash(point.x(), point.y())); var point2 = new Point(1, 2); System.out.println(point.equals(point2)); } } record Point(float x, float y) { } 

 > java --enable-preview --source 14 Main.java Note: Main.java uses preview language features. Note: Recompile with -Xlint:preview for details. Point[x=1.0, y=2.0] hashCode = -260046848 hashCode2 = -260045887 true 

Ainsi, toString() et equals() fonctionnent comme je m'y attendais (enfin, à moins que toString() n'utilise des crochets, mais je voudrais des accolades). Mais hashCode() fonctionne différemment. Pour une raison quelconque, j'ai pensé qu'il serait compatible avec Objects.hash() . Mais rien ne nous empêche de créer notre propre implémentation de hashCode() . Faisons-le, et en même temps, nous transférerons la méthode main() intérieur:

 public record Point(float x, float y) { @Override public int hashCode() { return Objects.hash(x, y); } public static void main(String[] args) { System.out.println(new Point(1, 2).hashCode()); } } 

 > java --enable-preview --source 14 Point.java Note: Point.java uses preview language features. Note: Recompile with -Xlint:preview for details. -260045887 

Ok Vérifions maintenant la durabilité du compilateur. Faisons quelque chose de incorrect, par exemple, ajoutons un champ:

 public record Point(float x, float y) { private float z; } 

 Point.java:2: error: field declaration must be static private float z; ^ (consider replacing field with record component) 

Vous ne pouvez donc ajouter que des champs statiques.

Je me demande ce qui se passera si vous rendez les composants final ? Seront-ils encore plus définitifs?

 public record Point(final float x, final float y) { } 

 Point.java:1: error: record components cannot have modifiers public record Point(final float x, final float y) { ^ Point.java:1: error: record components cannot have modifiers public record Point(final float x, final float y) { ^ 

C'est peut-être une interdiction logique. Pour qu'il n'y ait aucune illusion que les composants deviendront mutables si vous enlevez le final. Oui, et enum a une règle similaire, donc rien de nouveau:

 enum A { final X; // No modifiers allowed for enum constants } 

Et si vous remplacez le type de méthode d'accès?

 public record Point(float x, float y) { public double x() { return x; } } 

 Point.java:2: error: invalid accessor method in record Point public double x() { ^ (return type of accessor method x() is not compatible with type of record component x) 

C'est tout à fait logique.

Et si vous changez la visibilité?

 public record Point(float x, float y) { private float x() { return x; } } 

 Point.java:2: error: invalid accessor method in record Point private float x() { ^ (accessor method must be public) 

C’est également impossible.

L'héritage de classes est interdit, même d' Object :

 public record Point(float x, float y) extends Object { } 

 Point.java:1: error: '{' expected public record Point(float x, float y) extends Object { ^ 

Mais vous pouvez implémenter des interfaces:

 public record Point(float x, float y) implements PointLike { public static void main(String[] args) { PointLike point = new Point(1, 2); System.out.println(point.x()); System.out.println(point.y()); } } public interface PointLike { float x(); float y(); } 

 > java --enable-preview --source 14 Point.java Note: Point.java uses preview language features. Note: Recompile with -Xlint:preview for details. 1.0 2.0 

C'est intéressant de jouer un peu avec le constructeur canonique. Écrivons d'abord un constructeur canonique compact, c'est-à-dire constructeur canonique sans arguments, et ajoutez-y la validation des paramètres d'entrée:

 public record Point(float x, float y) { public Point { if (Float.isNaN(x) || Float.isNaN(y)) { throw new IllegalArgumentException("NaN"); } } public static void main(String[] args) { System.out.println(new Point(Float.NaN, 2)); } } 

 … Exception in thread "main" java.lang.IllegalArgumentException: NaN at Point.<init>(Point.java:4) at Point.main(Point.java:9) 

Gagné. Mais je me demande si cela fonctionnera si vous écrivez le même code, mais par return :

 public record Point(float x, float y) { public Point { if (!Float.isNaN(x) && !Float.isNaN(y)) { return; } throw new IllegalArgumentException("NaN"); } } 

 Point.java:2: error: invalid compact constructor in record Point(float,float) public Point { ^ (compact constructor must not have return statements) 

Un détail intéressant. Il est peu probable que cela me fasse beaucoup de mal dans la vie, car je ne suis pas un fan de l'écriture de return , mais tous les développeurs IDE doivent garder cela à l'esprit.

Essayons un constructeur canonique explicite. Je me demande s'il est possible de renommer les paramètres?

 public record Point(float x, float y) { public Point(float _x, float _y) { if (Float.isNaN(_x) || Float.isNaN(_y)) { throw new IllegalArgumentException("NaN"); } this.x = _x; this.y = _y; } } 

 Point.java:2: error: invalid canonical constructor in record Point public Point(float _x, float _y) { ^ (invalid parameter names in canonical constructor) 

Il s'avère que vous ne pouvez pas renommer. Mais je ne vois rien de mal à une telle restriction. Le code sera plus propre.

Et qu'en est-il de l'ordre d'initialisation?

 public record Point(float x, float y) { public Point { System.out.println(this); } public static void main(String[] args) { System.out.println(new Point(-1, 2)); } } 

 … Point[x=0.0, y=0.0] Point[x=-1.0, y=2.0] 

Le premier Point imprimé avec des zéros, ce qui signifie que les champs ont été affectés à la toute fin du constructeur, après System.out.println(this) .

Bon. Que diriez-vous d'ajouter un constructeur non canonique? Par exemple, un constructeur sans arguments:

 public record Point(float x, float y) { public Point() { } } 

 Point.java:2: error: constructor is not canonical, so its first statement must invoke another constructor public Point() { ^ 

Ouais, j'ai oublié d'écrire this(0, 0) . Mais nous ne corrigerons pas et ne vérifierons pas cela.

Et les génériques?

 public record Point<A extends Number>(A x, A y) { public static void main(String[] args) { System.out.println(new Point<>(-1, 2)); } } 

 > java --enable-preview --source 14 Point.java Note: Point.java uses preview language features. Note: Recompile with -Xlint:preview for details. Point[x=-1, y=2] 

Rien de surnaturel. Eh bien, sauf que vous devez vous rappeler que les paramètres de type doivent être définis avant d'enregistrer les paramètres.

Est-il possible de créer un enregistrement sans composants?

 public record None() { public static void main(String[] args) { System.out.println(new None()); } } 

 > java --enable-preview --source 14 None.java Note: None.java uses preview language features. Note: Recompile with -Xlint:preview for details. None[] 

Pourquoi pas.

Quelles choses n'avons-nous pas essayées? Qu'en est-il des publications imbriquées?

 record Point(int x, int y) { record Nested(int z) { void print() { System.out.println(x); } } } 

 Point.java:4: error: non-static record component x cannot be referenced from a static context System.out.println(x); ^ 

Cela signifie que les entrées imbriquées sont toujours statiques (comme enum ). Si oui, que faire si vous déclarez un enregistrement local? En théorie, alors il ne devrait pas capturer un contexte externe non statique:

 public class Main { public static void main(String[] args) { record Point(int x, int y) { void print() { System.out.println(Arrays.toString(args)); } } new Point(1, 2).print(); } } 

 > java --enable-preview --source 14 Main.java Note: Main.java uses preview language features. Note: Recompile with -Xlint:preview for details. [] 

Hmm, ça a marché. Je pense que c'est un bug. Ou juste une faille: ce comportement est hérité des classes locales ordinaires qui peuvent capturer des variables finales externes efficaces, mais ils ont oublié de les corriger pour les enregistrements.

Un point sensible qui m'intéresse: est-il possible de créer plusieurs enregistrements publics dans un même fichier?

 public record Point(float x, float y) { } public record Point2(float x, float y) { } 

 > javac --enable-preview --release 14 Point.java Point.java:4: error: class Point2 is public, should be declared in a file named Point2.java public record Point2(float x, float y) { ^ 

C'est impossible. Je me demande si ce sera un problème dans de vrais projets? Beaucoup voudront sûrement écrire beaucoup d'enregistrements afin de modéliser leurs entités. Ensuite, vous devrez tous les décomposer dans vos propres fichiers ou utiliser les enregistrements joints.

Enfin, je voudrais aussi jouer avec la réflexion. Comment au moment de l'exécution pour trouver des informations sur les composants qu'un enregistrement contient? Pour cela, vous pouvez utiliser la méthode Class.getRecordComponents () :

 import java.lang.reflect.RecordComponent; public record Point(float x, float y) { public static void main(String[] args) { var point = new Point(1, 2); for (RecordComponent component : point.getClass().getRecordComponents()) { System.out.println(component); } } } 

 > java --enable-preview --source 14 Point.java Note: Point.java uses preview language features. Note: Recompile with -Xlint:preview for details. float x float y 

J'ai également remarqué qu'en Java 14 un nouveau type d'annotation est apparu spécifiquement pour l'enregistrement des composants: ElementType.RECORD_COMPONENT . Et que se passe-t-il si vous utilisez les anciens types FIELD et PARAMETER ? Après tout, les composants ne semblent être ni des champs ni des paramètres:

 public record Point( @FieldAnnotation @ComponentAnnotation float x, @ParamAnnotation @ComponentAnnotation float y) { } @Target(ElementType.FIELD) @interface FieldAnnotation { } @Target(ElementType.PARAMETER) @interface ParamAnnotation { } @Target(ElementType.RECORD_COMPONENT) @interface ComponentAnnotation { } 

Oui, le code se compile, donc les trois fonctionnent. Eh bien, c'est logique. Je me demande s'ils seront "traînés" dans les champs?

 public record Point( @FieldAnnotation @ComponentAnnotation float x, @ParamAnnotation @ComponentAnnotation float y) { public static void main(String[] args) { var point = new Point(1, 2); Field[] fields = point.getClass().getDeclaredFields(); for (Field field : fields) { for (Annotation annotation : field.getAnnotations()) { System.out.println(field + ": " + annotation); } } } } @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @interface FieldAnnotation { } @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @interface ParamAnnotation { } @Target(ElementType.RECORD_COMPONENT) @Retention(RetentionPolicy.RUNTIME) @interface ComponentAnnotation { } 

 > java --enable-preview --source 14 Point.java Note: Point.java uses preview language features. Note: Recompile with -Xlint:preview for details. private final float Point.x: @FieldAnnotation() 

Cela signifie que seules les annotations FIELD sont « RECORD_COMPONENT », mais pas RECORD_COMPONENT et PARAMETER .

Je terminerai peut-être avec cela, car l'article est déjà apparu assez lourd. Il serait possible de «creuser» pendant longtemps et en profondeur, en testant toutes sortes de cas de bord différents, mais je pense que le niveau de profondeur actuel est plus que suffisant.

Conclusions


Les enregistrements sont sans aucun doute un élément cool et très attendu par la communauté qui nous fera gagner du temps à l'avenir et nous fera économiser une énorme quantité de code standard. Maintenant, les enregistrements sont presque prêts, et il ne reste plus qu'à attendre que certaines rugosités soient corrigées et que la version publique de Java 14 soit publiée. Vrai, alors vous devrez toujours attendre 1-2 versions lorsque les enregistrements deviendront stables, mais si vous le souhaitez, ils peuvent être utilisés en mode aperçu .

Et ceux qui ne sont pas pressés de passer de Java 8, je pense que nous devrions attendre jusqu'en septembre 2021 et passer immédiatement à Java 17, où il y aura déjà des expressions de commutation stables, des blocs de texte , des instances améliorées , des enregistrements et des types scellés (avec une forte probabilité).

PS Si vous ne voulez pas manquer mes nouvelles et articles sur Java, alors je vous recommande de vous abonner à ma chaîne dans Telegram .

Tout à venir!

Source: https://habr.com/ru/post/fr482300/


All Articles