آخر مرة اختبرنا فيها مشغل
instanceof
محسن ، والذي سيظهر في
الإصدار الرابع عشر القادم
من Java (سيتم إصداره في مارس 2020). أود اليوم أن أبحث بالتفصيل في الميزة النحوية الثانية ، والتي ستظهر أيضًا في Java 14: records.
تحتوي السجلات
على JEP خاصة بها ، ولكنها لا تعرض الكثير من التفاصيل ، لذلك هناك الكثير الذي يمكنك تجربته والتحقق منه بنفسك. نعم ، يمكنك بالطبع فتح
مواصفات Java SE ، لكن يبدو لي أنه من المثير جدًا أن تبدأ في كتابة التعليمات البرمجية بنفسك وإلقاء نظرة على سلوك المترجم في بعض المواقف. اصنعي طيور النورس وجلس بشكل مريح. دعنا نذهب.
بخلاف المرة الأخيرة ، عندما اضطررت إلى إنشاء فرع JDK خاص للاختبار على
instanceof
، الآن كل هذا موجود بالفعل في الفرع الرئيسي ومتاح في الإصدار
JDK 14 المبكر الذي قمت بتنزيله.
أولاً ، نطبق المثال الكلاسيكي باستخدام
Point
ونقوم بتجميعه:
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
جمعت بنجاح ملف
Point.class
. لنقم بتفكيكها ونرى ما قام المترجم بإنشائه هناك:
> 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(); }
نعم ، أنشأ المترجم ما يلي:
- الفئة النهائية الموروثة من java.lang.Record (على غرار
enum
، الذي يرث من java.lang.Enum
). - الحقول النهائية الخاصة
x
و y
. - مُنشئ عام يطابق توقيع السجل نفسه. ويسمى هذا المنشئ الكنسي .
- تطبيقات
toString()
، hashCode()
equals()
. ومن المثير للاهتمام أن hashCode()
و equals()
toString()
، و toString()
ليس كذلك. من غير المرجح أن يؤثر هذا على أي شيء ، لأن الفصل نفسه final
، لكن هل يعرف أحد لماذا فعلوا ذلك؟ (لست كذلك) - طرق قراءة الحقول.
مع المنشئ وطرق القراءة ، كل شيء واضح ، لكنني أتساءل كيف يتم تنفيذ
toString()
و
hashCode()
و
equals()
بالضبط؟ لنرى. للقيام بذلك ، قم بتشغيل
javap
باستخدام علامة
-verbose
:
دبوس طويل مفكك > 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
في تطبيق
toString()
،
hashCode()
و
equals()
نرى
invokedynamic
. هذا يعني أنه سيتم إنشاء منطق هذه الأساليب بتكاسل بواسطة الجهاز الظاهري نفسه. لست متخصصًا في وقت التشغيل ، لكنني أعتقد أن هذا يتم لتحقيق كفاءة أفضل. على سبيل المثال ، إذا توصلوا في المستقبل إلى علامة تجزئة أسرع ، في هذا النهج ، ستحصل الشفرة المترجمة القديمة على جميع مزايا الإصدار الجديد. كما أنه يقلل من حجم ملفات الفئة.
ولكن شيئا ذهبنا عميقا. دعنا نعود إلى سجلات
الأغنام لدينا. دعونا نحاول إنشاء مثيل
Point
ونرى كيف تعمل الطرق. من الآن فصاعدًا ، لن أستخدم
javac
وأعد تشغيل ملف java مباشرةً:
… 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()
و
equals()
كما كنت أتوقع (جيدًا ، ما لم يستخدم
toString()
الأقواس المربعة ، لكنني أود الأقواس المتعرجة). لكن
hashCode()
يعمل بشكل مختلف. لسبب ما ، أعتقد أنه سيكون متوافقًا مع
Objects.hash()
. لكن لا شيء يمنعنا من إنشاء تطبيقنا الخاص لـ
hashCode()
. دعونا نفعل ذلك ، وفي الوقت نفسه سننقل الطريقة
main()
الداخل:
… 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. الآن دعونا نتحقق من المترجم للتأكد من متانته. لنقم بشيء غير صحيح ، على سبيل المثال ، أضف حقلًا:
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)
لذلك ، يمكنك إضافة الحقول الثابتة فقط.
أتساءل ماذا سيحدث إذا قمت بإجراء المكونات
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) { ^
ربما هذا هو الحظر المنطقي. بحيث لا يوجد أي وهم بأن المكونات ستصبح قابلة للتغيير إذا قمت بإزالة النهائي. نعم ،
enum
له قاعدة مماثلة ، لذلك لا شيء جديد:
enum A { final X;
ماذا لو تجاوزت طريقة الوصول؟
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)
هذا منطقي تماما.
وإذا قمت بتغيير الرؤية؟
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)
إنه مستحيل أيضًا.
الوراثة من الفصول محظورة ، حتى من
Object
:
public record Point(float x, float y) extends Object { }
Point.java:1: error: '{' expected public record Point(float x, float y) extends Object { ^
ولكن يمكنك تنفيذ واجهات:
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
من الممتع أن تلعب قليلاً مع المنشئ القانوني. أولاً ، دعنا نكتب مُنشئًا قانونيًا مدمجًا ، أي مُنشئ قانوني بدون وسيطات ، وإضافة التحقق من صحة معلمات الإدخال هناك:
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)
انها ربحت. لكنني أتساءل عما إذا كانت ستنجح إذا كتبت الرمز نفسه ، ولكن من خلال
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)
تفاصيل مثيرة للاهتمام. من غير المحتمل أن يؤذيني هذا كثيرًا في الحياة ، حيث أنني لست من عشاق الكتابة ، لكن على جميع مطوري IDE أن يضعوا ذلك في الاعتبار.
دعونا نجرب منشئ قانوني واضح. وأتساءل عما إذا كان من الممكن إعادة تسمية المعلمات؟
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)
اتضح أنه لا يمكنك إعادة تسمية. لكني لا أرى شيئًا خاطئًا في مثل هذا التقييد. الرمز سيكون أكثر نظافة.
وماذا عن ترتيب التهيئة؟
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
طباعة First
Point
بأصفار ، مما يعني أنه تم تعيين الحقول في نهاية المنشئ ، بعد
System.out.println(this)
.
حسنا. ماذا عن إضافة مُنشئ غير قانوني؟ على سبيل المثال ، مُنشئ بدون وسيطات:
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() { ^
نعم ، نسيت أن تكتب
this(0, 0)
. لكننا لن نصحح ونتحقق من ذلك.
ماذا عن الوراثة؟
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]
لا شيء خارق. حسنًا ، باستثناء أنك تحتاج إلى تذكر أنه يجب تعيين معلمات الكتابة قبل تسجيل المعلمات.
هل من الممكن إنشاء سجل بدون مكونات؟
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[]
لم لا.
ما الأشياء التي لم نجربها؟ ماذا عن المشاركات المتداخلة؟
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); ^
هذا يعني أن الإدخالات المتداخلة ثابتة دائمًا (مثل
enum
). إذا كان الأمر كذلك ، فماذا لو كنت تعلن عن سجل محلي؟ من الناحية النظرية ، يجب ألا تلتقط سياقًا خارجيًا غير ثابت:
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. []
حسنًا ، لقد عملت. أعتقد أن هذا خطأ. أو مجرد خلل: هذا السلوك موروث من فصول محلية عادية يمكنها التقاط متغيرات نهائية خارجية فعالة ، لكنهم نسوا إصلاحها للسجلات.
نقطة حساسة تهمني: هل من الممكن إنشاء عدة سجلات عامة في ملف واحد؟
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) { ^
هذا مستحيل. أتساءل ما إذا كانت هذه ستكون مشكلة في المشاريع الحقيقية؟ بالتأكيد سيرغب الكثيرون في كتابة الكثير من السجلات لتصميم كياناتهم. ثم سيتعين عليك تحليلها جميعًا في ملفاتك الخاصة ، أو استخدام السجلات المرفقة.
أخيرًا ، أود أيضًا أن ألعب بالتفكير. كيف في وقت التشغيل لمعرفة معلومات حول المكونات التي يحتوي على سجل؟ لهذا ، يمكنك استخدام الأسلوب
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
لاحظت أيضًا أنه في Java 14 ، ظهر نوع جديد من التعليقات التوضيحية خصيصًا لتسجيل المكونات:
ElementType.RECORD_COMPONENT . وماذا يحدث إذا استخدمت الأنواع القديمة
FIELD
و
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 { }
نعم ، يتم تجميع الشفرة ، لذا تعمل الثلاثة. حسنًا ، هذا منطقي. أتساءل عما إذا كان سيتم "جرهم" إلى الحقول؟
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()
وهذا يعني أن التعليقات التوضيحية
FIELD
هي فقط "
RECORD_COMPONENT
" ، ولكن ليس
RECORD_COMPONENT
و
PARAMETER
.
ربما سأنتهي بهذا ، لأن المقال قد بدا مرهقًا بالفعل. يمكن للمرء أن "يحفر" لفترة طويلة وعميق ، واختبار جميع أنواع الحالات المختلفة ، لكنني أعتقد أن المستوى الحالي للعمق أكثر من كاف.
النتائج
مما لا شك فيه أن السجلات شيء رائع ومتوقع للغاية من جانب المجتمع الذي سيوفر لنا وقتًا في المستقبل ويوفر علينا قدرًا كبيرًا من التعليمات البرمجية للملف. الآن أصبحت التسجيلات جاهزة تقريبًا ، ويظل فقط في انتظار إصلاح
بعض الدرجات الخشنة وإصدار الإصدار العمومي من Java 14. صحيحًا ، ستظل بحاجة إلى الانتظار حتى الإصدارين الأول والثاني عندما تصبح التسجيلات مستقرة ، ولكن إذا كنت ترغب في ذلك ، فيمكنك استخدامها في وضع المعاينة .
وأولئك الذين ليسوا في عجلة من أمرهم للتبديل من Java 8 ، أعتقد أننا يجب أن ننتظر حتى سبتمبر 2021 ، وأن نتحول على الفور إلى Java 17 ، حيث ستكون هناك بالفعل
تعبيرات تبديل مستقرة ،
وكتل نصية ،
ومثيل محسن ،
وسجلات وأنواع مختومة (مع احتمال كبير).
ملاحظة: إذا كنت لا تريد أن تفوت الأخبار والمقالات حول جافا ، فنوصيك بالاشتراك في
قناتي في Telegram .
كل ذلك مع المجيء!