Java 14-Datensätze erkunden

Das letzte Mal haben wir die verbesserte instanceof Operators getestet, die in der kommenden, 14. Version von Java (wird im März 2020 veröffentlicht) erscheinen wird. Heute möchte ich das zweite syntaktische Merkmal, das auch in Java 14 vorkommt, genauer untersuchen: Datensätze.

Die Datensätze verfügen über ein eigenes JEP , es werden jedoch nur wenige Details angezeigt. Daher gibt es viele Möglichkeiten, dies selbst zu überprüfen. Ja, Sie können natürlich die Java SE- Spezifikation öffnen, aber es scheint mir viel interessanter, selbst mit dem Schreiben von Code zu beginnen und das Verhalten des Compilers in bestimmten Situationen zu untersuchen. Stellen Sie also eine Möwe her und setzen Sie sich bequem hin. Lass uns gehen.

Im Gegensatz zum letzten Mal, als ich eine spezielle JDK-Verzweigung zum instanceof für Tests erstellen musste, befindet sich all dies jetzt bereits in der Hauptverzweigung und ist in der frühen JDK 14-Version verfügbar, die ich heruntergeladen habe.

Zuerst implementieren wir das klassische Beispiel mit Point und kompilieren es:

 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 hat die Datei Point.class erfolgreich kompiliert. Zerlegen wir es und sehen, was der Compiler dort für uns generiert hat:

 > 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(); } 

Ja, der Compiler hat Folgendes erstellt:

  • Die letzte von java.lang.Record geerbte Klasse (ähnlich wie enum , die von java.lang.Enum erbt).
  • Private Endfelder x und y .
  • Ein öffentlicher Konstruktor, der der Signatur des Datensatzes selbst entspricht. Ein solcher Konstruktor heißt kanonisch .
  • Implementierungen von toString() , hashCode() und equals() . Interessanterweise sind hashCode() und equals() final und toString() nicht. Es ist unwahrscheinlich, dass sich dies auf etwas auswirkt, da die Klasse selbst final ist. Aber weiß jemand, warum sie dies getan hat? (Bin ich nicht)
  • Methoden zum Lesen von Feldern.

Mit dem Konstruktor und den Lesemethoden ist alles klar, aber ich frage mich, wie genau toString() , hashCode() und equals() implementiert sind. Mal sehen Führen Sie dazu javap mit dem Flag -verbose :

Langer Disassembler Pin
 > 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 

In der Implementierung von toString() , hashCode() und equals() wir invokedynamic . Dies bedeutet, dass die Logik dieser Methoden träge von der virtuellen Maschine selbst generiert wird. Ich bin kein großer Laufzeitspezialist, aber ich denke, dass dies für eine bessere Effizienz getan wird. Wenn sie beispielsweise in Zukunft einen schnelleren Hash finden, werden bei diesem Ansatz alle Vorteile der neuen Version für den alten kompilierten Code genutzt. Es reduziert auch die Größe von Klassendateien.

Aber wir sind etwas zu tief gegangen. Kommen wir zurück zu unseren Schafen . Versuchen wir, einen Point zu instanziieren und zu sehen, wie die Methoden funktionieren. Ab sofort verwende ich kein Java mehr und javac die Java-Datei direkt:

 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 

toString() und equals() funktionieren also wie erwartet ( toString() , es sei denn, toString() verwendet eckige Klammern, aber ich möchte geschweifte Klammern verwenden). hashCode() funktioniert jedoch anders. Aus irgendeinem Grund dachte ich, dass es mit Objects.hash() kompatibel Objects.hash() . Nichts hindert uns daran, eine eigene Implementierung von hashCode() erstellen. Lassen Sie es uns tun und gleichzeitig werden wir die main() -Methode nach innen übertragen:

 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 Überprüfen wir nun den Compiler auf Haltbarkeit. Lassen Sie uns etwas falsch machen, zum Beispiel ein Feld hinzufügen:

 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) 

Sie können also nur statische Felder hinzufügen.

Ich frage mich, was passiert, wenn Sie die Komponenten final ? Werden sie noch endgültiger sein?

 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) { ^ 

Vielleicht ist das ein logisches Verbot. Damit es keine Illusion gibt, dass die Komponenten veränderlich werden, wenn Sie final entfernen. Ja, und enum hat eine ähnliche Regel, also nichts Neues:

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

Was ist, wenn Sie die Art der Zugriffsmethode überschreiben?

 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) 

Das ist absolut logisch.

Und wenn Sie die Sichtbarkeit ändern?

 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) 

Es ist auch unmöglich.

Das Erben von Klassen ist verboten, auch von Object :

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

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

Sie können jedoch Schnittstellen implementieren:

 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 

Es ist interessant, ein wenig mit dem kanonischen Konstruktor zu spielen. Schreiben wir zunächst einen kompakten kanonischen Konstruktor, d.h. kanonischer Konstruktor ohne Argumente, und fügen Sie dort die Validierung der Eingabeparameter hinzu:

 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) 

Verdient Aber ich frage mich, ob es funktionieren wird, wenn Sie denselben Code schreiben, aber durch 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) 

Ein interessantes Detail. Es ist unwahrscheinlich, dass mir das im Leben weh tut, da ich kein Fan des Schreibens von return bin, aber alle IDE-Entwickler müssen dies berücksichtigen.

Versuchen wir einen expliziten kanonischen Konstruktor. Ich frage mich, ob es möglich ist, die Parameter umzubenennen?

 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) 

Es stellt sich heraus, dass Sie nicht umbenennen können. An einer solchen Einschränkung sehe ich aber nichts auszusetzen. Der Code wird sauberer.

Und was ist mit der Initialisierungsreihenfolge?

 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] 

First Point mit Nullen gedruckt, was bedeutet, dass die Felder ganz am Ende des Konstruktors nach System.out.println(this) zugewiesen wurden.

Gut Wie wäre es, einen nicht-kanonischen Konstruktor hinzuzufügen? Zum Beispiel ein Konstruktor ohne Argumente:

 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() { ^ 

Ja, habe vergessen, this(0, 0) zu schreiben this(0, 0) . Wir werden dies jedoch nicht korrigieren und überprüfen.

Was ist mit Generika?

 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] 

Nichts Übernatürliches. Nun, außer dass Sie sich daran erinnern müssen, dass Typparameter eingestellt werden müssen, bevor Parameter aufgezeichnet werden.

Ist es möglich, einen Datensatz ohne Komponenten zu erstellen?

 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[] 

Warum nicht.

Welche Dinge haben wir nicht ausprobiert? Was ist mit verschachtelten Posts?

 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); ^ 

Dies bedeutet, dass verschachtelte Einträge immer statisch sind (wie enum ). Wenn ja, was ist, wenn Sie einen lokalen Datensatz deklarieren? Theoretisch sollte es dann keinen externen nicht statischen Kontext erfassen:

 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, es hat funktioniert. Ich denke das ist ein Bug. Oder nur ein Fehler: Dieses Verhalten wird von gewöhnlichen lokalen Klassen geerbt, die externe, effektiv endgültige Variablen erfassen können, aber vergessen haben, sie für Datensätze zu reparieren.

Ein wunder Punkt, der mich interessiert: Ist es möglich, mehrere öffentliche Datensätze in einer Datei zu erstellen?

 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) { ^ 

Es ist unmöglich Ich frage mich, ob dies in echten Projekten ein Problem sein wird. Sicherlich werden viele viele Datensätze schreiben wollen, um ihre Entitäten zu modellieren. Dann müssen Sie alle in Ihre eigenen Dateien zerlegen oder die angehängten Datensätze verwenden.

Schließlich möchte ich auch mit Reflexion spielen. Wie kann ich zur Laufzeit Informationen zu den Komponenten in einem Datensatz abrufen? Hierzu können Sie die Class.getRecordComponents () -Methode verwenden:

 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 

Ich habe auch festgestellt, dass in Java 14 eine neue Art von Annotation speziell für das Aufzeichnen von Komponenten angezeigt wurde : ElementType.RECORD_COMPONENT . Und was passiert, wenn Sie die alten Typen FIELD und PARAMETER ? Schließlich scheinen Komponenten weder Felder noch Parameter zu sein:

 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 { } 

Ja, der Code wird kompiliert, also funktionieren alle drei. Das ist logisch. Ich frage mich, ob sie in die Felder "gezogen" werden?

 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() 

Dies bedeutet, dass nur FIELD Annotationen „gezogen“ werden, nicht jedoch RECORD_COMPONENT und PARAMETER .

Vielleicht werde ich damit enden, weil der Artikel schon ziemlich umständlich erschienen ist. Es wäre möglich, lange und tief zu „graben“ und alle möglichen Randfälle zu testen, aber ich denke, der aktuelle Tiefengrad ist mehr als genug.

Schlussfolgerungen


Aufzeichnungen sind zweifellos eine coole und von der Community sehr erwartete Sache, die uns in Zukunft Zeit und eine riesige Menge an Kessel-Code sparen wird. Jetzt sind die Aufzeichnungen fast fertig und es muss nur noch gewartet werden, bis einige Ungenauigkeiten behoben und die öffentliche Version von Java 14 freigegeben wurden. Richtig, dann müssen Sie auch 1-2 Veröffentlichungen warten, wenn die Aufzeichnungen stabil sind, aber Sie können sie im Vorschaumodus verwenden, wenn Sie dies wünschen .

Und diejenigen, die es nicht eilig haben, von Java 8 zu wechseln, sollten bis September 2021 warten und sofort zu Java 17 wechseln, wo es bereits stabile Switch-Ausdrücke , Textblöcke , verbesserte Instanzen , Datensätze und versiegelte Typen gibt (mit hoher Wahrscheinlichkeit).

PS Wenn Sie meine Neuigkeiten und Artikel über Java nicht missen möchten, empfehle ich Ihnen, meinen Kanal in Telegram zu abonnieren.

Alles mit dem Kommen!

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


All Articles