上次我们测试了改进的
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
)。 - 私有最终字段
x
和y
。 - 与记录本身的签名匹配的公共构造函数。 这样的构造函数称为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;
如果您覆盖访问方法的类型怎么办?
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 。 如果使用旧类型的
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
和
PARAMETER
。
也许我会以此结束,因为这篇文章看起来已经很麻烦了。 可能需要长时间深入地进行“挖掘”,以测试各种不同的边缘情况,但我认为当前的深度水平已绰绰有余。
结论
记录无疑是社区中的一件很酷的事情,并且非常值得期待,它将为我们节省未来的时间,并为我们节省大量的样板代码。 现在记录已经差不多准备好了,只需要等待修复
一些问题并发布Java 14的公开发布,是的,那么当记录变得稳定时,您仍然需要等待1-2个发布,但是如果您愿意,可以在预览模式下使用它们。 。
那些不急于从Java 8切换的人,我认为我们应该等到2021年9月,并立即切换到Java 17,那里已经有稳定的
切换表达式 ,
文本块 ,
改进的instanceof ,
记录和
密封类型 (很有可能)。
PS:如果您不想错过有关Java的新闻和文章,那么建议您订阅
Telegram中的频道 。
一切即将到来!