Menjelajahi Java 14 Records

Terakhir kali kami menguji instanceof operator yang lebih baik, yang akan muncul di versi Jawa 14 yang akan datang (akan dirilis pada Maret 2020). Hari ini saya ingin menyelidiki secara terperinci fitur sintaksis kedua, yang juga akan muncul dalam catatan Java 14:.

Rekaman memiliki JEP mereka sendiri , tetapi tidak menunjukkan banyak detail, jadi ada banyak untuk mencoba dan memverifikasi sendiri. Ya, Anda tentu saja dapat membuka spesifikasi Java SE, tetapi bagi saya tampaknya jauh lebih menarik untuk mulai menulis kode sendiri dan melihat perilaku kompiler dalam situasi tertentu. Jadi buat burung camar dan duduk dengan nyaman. Ayo pergi.

Tidak seperti yang terakhir kali, ketika saya harus membangun cabang JDK khusus untuk pengujian instanceof , sekarang semua ini sudah ada di cabang utama dan tersedia di build JDK 14 awal , yang saya unduh.

Pertama, kami mengimplementasikan contoh klasik dengan Point dan mengkompilasinya:

 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 berhasil mengompilasi file Point.class . Mari kita bongkar dan lihat apa yang dihasilkan kompiler untuk kita di sana:

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

Ya, kompiler membuat yang berikut ini:

  • Kelas terakhir diwarisi dari java.lang.Record (mirip dengan enum , yang diwarisi dari java.lang.Enum ).
  • Kolom terakhir pribadi x dan y .
  • Konstruktor publik yang cocok dengan tanda tangan catatan itu sendiri. Konstruktor semacam itu disebut kanonik .
  • Implementasi toString() , hashCode() dan equals() . Menariknya, hashCode() dan equals() adalah final , dan toString() tidak. Ini tidak mungkin memengaruhi apa pun, karena kelas itu sendiri sudah final , tetapi apakah ada yang tahu mengapa mereka melakukan ini? (Saya tidak)
  • Metode untuk bidang membaca.

Dengan konstruktor dan metode membaca, semuanya jelas, tetapi saya bertanya-tanya bagaimana tepatnya toString() , hashCode() dan equals() diimplementasikan? Ayo lihat. Untuk melakukan ini, jalankan javap dengan flag -verbose :

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

Dalam implementasi toString() , hashCode() dan equals() kita lihat invokedynamic . Ini berarti bahwa logika dari metode ini akan dihasilkan dengan malas oleh mesin virtual itu sendiri. Saya bukan spesialis runtime yang hebat, tetapi saya pikir ini dilakukan untuk efisiensi yang lebih baik. Sebagai contoh, jika di masa depan mereka datang dengan hash yang lebih cepat, maka dalam pendekatan ini kode yang dikompilasi lama akan mendapatkan semua keuntungan dari versi baru. Ini juga mengurangi ukuran file kelas.

Tapi sesuatu yang kami lakukan terlalu dalam. Mari kita kembali ke catatan domba kita. Mari kita coba instantiate Point dan lihat bagaimana metodenya bekerja. Mulai sekarang, saya tidak akan lagi menggunakan javac dan jalankan file java secara langsung:

 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 

Jadi, toString() dan equals() berfungsi seperti yang saya harapkan (well, kecuali toString() menggunakan tanda kurung siku, tapi saya ingin kurung kurawal). Tetapi hashCode() bekerja secara berbeda. Untuk beberapa alasan, saya pikir itu akan kompatibel dengan Objects.hash() . Tetapi tidak ada yang menghalangi kita untuk membuat implementasi hashCode() kita sendiri hashCode() . Mari kita lakukan, dan pada saat yang sama kita akan mentransfer metode main() di dalam:

 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 Sekarang mari kita periksa kompiler untuk daya tahan. Mari kita lakukan sesuatu yang salah, misalnya, tambahkan bidang:

 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) 

Jadi, Anda hanya dapat menambahkan bidang statis.

Saya bertanya-tanya apa yang akan terjadi jika Anda membuat komponen jadi final ? Apakah mereka akan lebih final?

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

Mungkin ini adalah larangan logis. Sehingga tidak ada ilusi bahwa komponen akan menjadi bisa berubah jika Anda menghapus final. Ya, dan enum memiliki aturan yang sama, jadi tidak ada yang baru:

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

Bagaimana jika Anda mengganti jenis metode akses?

 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) 

Ini sangat logis.

Dan jika Anda mengubah visibilitas?

 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) 

Itu juga tidak mungkin.

Warisan dari kelas dilarang, bahkan dari Object :

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

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

Tetapi Anda dapat mengimplementasikan antarmuka:

 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 

Sangat menarik untuk bermain sedikit dengan konstruktor kanonik. Pertama, mari kita menulis konstruktor kanonik yang kompak, mis. konstruktor kanonik tanpa argumen, dan menambahkan validasi parameter input di sana:

 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) 

Berpenghasilan. Tapi saya ingin tahu apakah ini akan berhasil jika Anda menulis kode yang sama, tetapi melalui 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) 

Detail yang menarik. Ini tidak mungkin menyakiti saya dalam hidup, karena saya bukan penggemar menulis return , tetapi semua pengembang IDE perlu mengingat hal ini.

Mari kita coba konstruktor kanonik eksplisit. Saya ingin tahu apakah mungkin untuk mengubah nama parameter?

 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) 

Ternyata Anda tidak bisa mengganti nama. Tapi saya tidak melihat ada yang salah dengan pembatasan seperti itu. Kode akan lebih bersih.

Dan bagaimana dengan urutan inisialisasi?

 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] 

Point dicetak dengan nol, yang berarti bahwa bidang-bidang tersebut diberikan di bagian paling akhir konstruktor, setelah System.out.println(this) .

Bagus Bagaimana kalau menambahkan konstruktor noncanonical? Misalnya, konstruktor tanpa argumen:

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

Ya, lupa menulis this(0, 0) . Tetapi kami tidak akan memperbaiki dan memverifikasi ini.

Bagaimana dengan obat generik?

 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] 

Tidak ada yang supranatural. Nah, kecuali Anda perlu ingat bahwa parameter type harus diatur sebelum merekam parameter.

Apakah mungkin membuat catatan tanpa komponen?

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

Kenapa tidak

Hal-hal apa yang belum kita coba? Bagaimana dengan postingan bersarang?

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

Ini berarti bahwa entri bersarang selalu statis (seperti enum ). Jika demikian, bagaimana jika Anda mendeklarasikan catatan lokal? Secara teori, maka seharusnya tidak menangkap konteks non-statis eksternal:

 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, berhasil. Saya pikir ini adalah bug. Atau hanya cacat: perilaku ini diwarisi dari kelas lokal biasa yang dapat menangkap variabel akhir eksternal yang efektif, tetapi mereka lupa untuk memperbaikinya untuk catatan.

Satu poin penting yang menarik minat saya: apakah mungkin membuat beberapa catatan publik dalam satu file?

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

Itu tidak mungkin. Saya bertanya-tanya apakah ini akan menjadi masalah dalam proyek nyata? Tentunya banyak yang ingin menulis banyak catatan untuk memodelkan entitas mereka. Maka Anda harus menguraikan semuanya menjadi file Anda sendiri, atau menggunakan catatan terlampir.

Akhirnya, saya juga ingin bermain dengan refleksi. Bagaimana saat runtime untuk mengetahui informasi tentang komponen yang berisi catatan? Untuk ini, Anda dapat menggunakan metode 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 

Saya juga memperhatikan bahwa di Jawa 14 jenis anotasi baru muncul secara khusus untuk merekam komponen: ElementType.RECORD_COMPONENT . Dan apa yang terjadi jika Anda menggunakan FIELD dan PARAMETER tipe lama? Setelah semua, komponen tampaknya bukan bidang atau parameter:

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

Ya, kode mengkompilasi, jadi ketiganya berfungsi. Ya, itu masuk akal. Saya ingin tahu apakah mereka akan "diseret" ke ladang?

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

Ini berarti bahwa hanya anotasi FIELD yang "diseret", tetapi bukan RECORD_COMPONENT dan PARAMETER .

Mungkin saya akan mengakhiri dengan ini, karena artikelnya sudah tampak agak rumit. Mungkin untuk "menggali" untuk waktu yang lama dan dalam, menguji semua jenis kasus tepi yang berbeda, tapi saya pikir tingkat kedalaman saat ini lebih dari cukup.

Kesimpulan


Catatan tidak diragukan lagi adalah hal yang keren dan sangat diharapkan oleh komunitas yang akan menghemat waktu kita di masa depan dan menghemat banyak kode boilerplate. Sekarang rekaman sudah hampir siap, dan hanya tinggal menunggu beberapa kekasaran untuk diperbaiki dan rilis publik Java 14 akan dirilis. Benar, maka Anda masih perlu menunggu 1-2 rilis ketika rekaman menjadi stabil, tetapi jika Anda mau, mereka dapat digunakan dalam mode pratinjau .

Dan mereka yang tidak terburu-buru untuk beralih dari Java 8, saya pikir kita harus menunggu sampai September 2021, dan segera beralih ke Java 17, di mana sudah ada ekspresi saklar yang stabil, blok teks , instans instanceof yang lebih baik , catatan dan tipe yang disegel (dengan probabilitas tinggi).

PS Jika Anda tidak ingin ketinggalan berita dan artikel saya tentang Java, maka saya sarankan Anda berlangganan saluran saya di Telegram .

Semua dengan kedatangan!

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


All Articles