探索Java 14记录

上次我们测试了改进的instanceof运算符,该运算符将出现在即将发布的 Java 第14版 (将于2020年3月发布)中。 今天,我想详细研究第二种语法功能,该功能也将出现在Java 14:记录中。

唱片有自己的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 )。
  • 私有最终字段xy
  • 与记录本身的签名匹配的公共构造函数。 这样的构造函数称为canonical
  • toString()hashCode()equals() 。 有趣的是, hashCode()equals()final ,而toString()不是。 由于类本身是final ,所以这不太可能影响任何事情,但是有人知道他们为什么这样做吗? (我不是)
  • 读取字段的方法。

使用构造函数和读取方法,一切都清晰了,但是我想知道如何实现toString()hashCode()equals()吗? 让我们看看。 为此,请使用-verbose标志运行javap

长拆装机销
 > 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 

好啦 现在让我们检查编译器的持久性。 让我们做一些不正确的事情,例如,添加一个字段:

 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) 

因此,您只能添加静态字段。

我不知道如果将组件定型会怎样? 他们会更最终吗?

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

也许这是合乎逻辑的禁令。 因此,如果您删除了final,就不会有组件会变得可变的幻想。 是的, enum也有类似的规则,因此没有新内容:

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

如果您覆盖访问方法的类型怎么办?

 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) 

一个有趣的细节。 因为我不喜欢写return ,所以这不太可能伤害我一生,但是所有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] 

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 。 如果使用旧类型的FIELDPARAMETER会发生什么? 毕竟,组件似乎既不是字段也不是参数:

 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_COMPONENTPARAMETER

也许我会以此结束,因为这篇文章看起来已经很麻烦了。 可能需要长时间深入地进行“挖掘”,以测试各种不同的边缘情况,但我认为当前的深度水平已绰绰有余。

结论


记录无疑是社区中的一件很酷的事情,并且非常值得期待,它将为我们节省未来的时间,并为我们节省大量的样板代码。 现在记录已经差不多准备好了,只需要等待修复一些问题并发布Java 14的公开发布,是的,那么当记录变得稳定时,您仍然需要等待1-2个发布,但是如果您愿意,可以在预览模式下使用它们。 。

那些不急于从Java 8切换的人,我认为我们应该等到2021年9月,并立即切换到Java 17,那里已经有稳定的切换表达式文本块改进的instanceof记录密封类型 (很有可能)。

PS:如果您不想错过有关Java的新闻和文章,那么建议您订阅Telegram中的频道

一切即将到来!

Source: https://habr.com/ru/post/zh-CN482300/


All Articles