01、什么是Record?
Record 是Java新增的库类,在Java 14和Java 15中以预览(preview)形式公布。Record类用来自动生成对定义数据进行创建、设置、访问以及比较等代码,所以又被称作数据类(data class)。在一些编程语言中,例如Kotlin,已经使用数据类来处理数据模式建立(Object Relational Mapping-ORM)以及传输(Data Transfer Objects-DTOs)等处理。Record类似于Java 的枚举类(Enum),用来简化、定义和处理数据。传统的枚举类的编程方式和自动生成代码的Record类,这两者使得Java编程在保持简单性和灵活性中相互平衡和补充。
02、为什么支持Record?
在应用软件开发中,编程人员经常会针对底层数据,进行对数据的构造器、访问方法(getters)、覆盖方法equals、覆盖方法hashCode、以及覆盖方法toString进行基础和重复性的编程。而使用Record类,程序中则可省去这些代码,而由支持Record的编译器自动生成。这不但提高了编程效率,而且提高了代码的可靠性。Record类也可以提高数据在ORM和DTOs的一致性(persistency)要求。例如,使用private对数据的定义更加安全和规范化;自动生成的equals方法更加标准化和可靠;对生成的数据类定义为final,因而限制对创建后的数据类的继承以及对已定义数据的修改等等。这些特性无疑提高了代码的标准化,使得对数据类的编程更加高效和可靠。
03、Record编程举例
例1. 利用Record创建一个名为Circle的数据类,产生对其半径radius进行基本处理功能。如同Java的任何类一样,Record从Object类继承而来:
java.lang.Object
java.lang.Record
Record类提供一个特殊的构造器:
protected Record()
利用Record创建数据类时,其参数可以是任何类型、以及任何数目的数据。先讨论一个简单例子,如:
record Circle(double radius) {} ; //创建并自动生成Circle构造器以及相关方法代码
对以上代码编译后,将自动生成如下代码并将生成的Circle类定义为final,即我们不能用它来继承子类,也不可以修改数据radius的值:
private final double radius = 0.0; //定义数据
public Circle(double radius) { //创建构造器
this.radius = radius;
}
public double radius() { //创建访问方法
return radius;
}
@Override
public boolean equals(Object obj) { //覆盖equals方法
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass())
return false;
circle = (Circle) obj;
if (radius != circle.radius())
return false;
else
return true;
}
@Override
public int hashCode() { //覆盖哈西方法
return Objects.hash(radius); //返回哈西代码
}
@Override
public String toString() { //覆盖toString方法
return “Circle [radius= “ + radius + “]”;
}
以上代码都是编译器自动生成的。在应用中我们可以直接调用这些方法,如:
package Example;
public class CircleApp {
public static void main(String[] args) {
@SuppressWarnings("preview")
record Circle(double radius) {};
Circle circle = new Circle(2.345);
System.out.println(circle.radius());
Circle circle2 = new Circle(3.33); //创建对象
System.out.println(circle.equals(circle2)); //false
System.out.println(circle.hashCode()); //显示哈西代码
System.out.println(circle); //Circle [radius=2.345]
}
}
以上利用Record创建Circle类的具体代码和运行结果见图1所示。
▍图1. 在Eclipse中运行Record创建的Circle类以及运行结果
例2. 利用Record创建一个名为Person的类。Person包括两个数据类,具有数据name以及ID的Student类和具有name和credit的Teacher类。并调用自动生成的方法进行测试。Person类的代码如下:
@SuppressWarnings("preview")
record Student(String name, String ID) {}; //定义数据类Student
@SuppressWarnings("preview")
record Teacher(String name, int credit) {};//定义数据类Teacher
对以上定义数据类Student和Teacher的代码编译后,将自动生成如下构造器和方法的代码:
Student类:
@SuppressWarnings("preview")
record Student(String name, String ID) {}; //定义数据类Student
@SuppressWarnings("preview")
record Teacher(String name, int credit) {};//定义数据类Teacher
对以上定义数据类Student和Teacher的代码编译后,将自动生成如下构造器和方法的代码:
Student类:
private final String name = null; 定义数据
private final String ID = null;
public Student (String name, String ID); //创建构造器
public String name(); //创建方法
public String ID();
public blooean equals(Object obj); //覆盖equals方法
public int hashCode() //覆盖哈西方法
public String toString(); //覆盖toString方法
Teacher类:
private final String name = null; 定义数据
private final int credit = 0;
public Teacher (String name, int credit); //构造器
public String name(); //访问方法
public int credit();
public blooean equals(Object obj); //覆盖equals方法
public int hashCode() //覆盖哈西方法
public String toString(); //覆盖toString方法
值得指出的是,由于Student和Teacher各自都有有两个数据,在Student中覆盖后的equals方法代码如下:
@Override
public boolean equals(Object obj) { //覆盖equals方法
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass())
return false;
person = (Person) obj;
return (name.compareTo(person.name()) && ID.compareTo(person.ID()
&& ID.compareTo(person.ID());
}
在Teacher中覆盖后的equals方法代码如下:
@Override
public boolean equals(Object obj) { //覆盖equals方法
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass())
return false;
person = (Person) obj;
return (name.compareTo(person.name()) && (credit
== person.credit());
}
以上利用Record创建数据类Student和Teacher以及其测试程序如图2所示。
▍图2. 在Eclipse中运行record创建的数据类
这个程序的运行结果如下:
Wang Lin
112233
false
-2104304052
Student[name=Li Gong, ID=445566]
Name = Dr. Zhang, credit = 10, Count = 0
Name = Mr. Qian, credit = 8, Count = 0
1
2
-793483106
-1753498516
false
Teacher[name=Mr. Qian, credit=8]
04、可以在Record创建数据类时加入其他代码吗?
我们只可以增添静态数据和静态方法以及对数据的访问方法。以Circle2为例:
@SuppressWarnings("preview")
record Circle2(double radius) {
static int count; //加入静态数据
public static void doCount() { //加入静态方法
count++;
}
public static int getCount() { //静态数据访问方法
return count;
}
public String getDiameter() { //加入对数的访问方法
return "Diameter = " + radius *2;
}
}
以上扩充或修改不会影响Record对Circle2的创建和自动生成的各个方法。其测试程序和运行结果截图如图3所示:
▍图3. 在Eclipse中运行record创建的数据类Circle2
05、解析Java编译器对创建数据类产生的代码(提高篇)
在操作系统中利用Java编译指令javap可以观察编译器在创建数据类时产生的详细代码描述和规范清单。这个代码清单用可能超出我们对Java应用程序的编程范围,这里只是给读者提供一个概况性的解释。
我们以例1中讨论过的Circle为例,在操作系统中进入储存Circl的目录,打入如下编译指令:
javac --enable-preview --release 15 Circle.java
编译器将显示如下提示信息:
Note: Circle.java uses preview language features.
Note: Recompile with -Xlint:preview for details.
表示正在使用Java的预览特性编译代码。再打入如下执行指令:
javap -v -p Circle.class
如图4所示。
▍图4. 在操作系统中打入指定的javac指令编译预览库类
产生java编译器生成的文件Circle.class后,打入:
javap -v -p Circle.class
这个指令将显示JVM生成的对数据类Circle的详细代码规范清单,如下所示:
C:\Users\ygao\eclipse-workspace\myProject\src\example>javap -v -p Circle.class
Classfile /C:/Users/ygao/eclipse-workspace/myProject/src/example/Circle.class
Last modified Jan 5, 2021; size 1096 bytes
MD5 checksum a7717f26f0812dac23eb89a37ccbc91d
Compiled from "Circle.java"
final class example.Circle extends java.lang.Record
minor version: 65535
major version: 59
flags: (0x0030) ACC_FINAL, ACC_SUPER
this_class: #8 // example/Circle
super_class: #2 // java/lang/Record
interfaces: 0, fields: 1, methods: 5, 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 // example/Circle.radius:D
#8 = Class #10 // example/Circle
#9 = NameAndType #11:#12 // radius:D
#10 = Utf8 example/Circle
#11 = Utf8 radius
#12 = Utf8 D
#13 = InvokeDynamic #0:#14 // #0:toString:(Lexample/Circle;)Ljava/lang/String;
#14 = NameAndType #15:#16 // toString:(Lexample/Circle;)Ljava/lang/String;
#15 = Utf8 toString
#16 = Utf8 (Lexample/Circle;)Ljava/lang/String;
#17 = InvokeDynamic #0:#18 // #0:hashCode:(Lexample/Circle;)I
#18 = NameAndType #19:#20 // hashCode:(Lexample/Circle;)I
#19 = Utf8 hashCode
#20 = Utf8 (Lexample/Circle;)I
#21 = InvokeDynamic #0:#22 // #0:equals:(Lexample/Circle;Ljava/lang/Object;)Z
#22 = NameAndType #23:#24 // equals:(Lexample/Circle;Ljava/lang/Object;)Z
#23 = Utf8 equals
#24 = Utf8 (Lexample/Circle;Ljava/lang/Object;)Z
#25 = Utf8 (D)V
#26 = Utf8 Code
#27 = Utf8 LineNumberTable
#28 = Utf8 MethodParameters
#29 = Utf8 ()Ljava/lang/String;
#30 = Utf8 ()I
#31 = Utf8 (Ljava/lang/Object;)Z
#32 = Utf8 ()D
#33 = Utf8 SourceFile
#34 = Utf8 Circle.java
#35 = Utf8 Record
#36 = Utf8 BootstrapMethods
#37 = MethodHandle 6:#38 // 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;
#38 = Methodref #39.#40 // 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;
#39 = Class #41 // java/lang/runtime/ObjectMethods
#40 = NameAndType #42:#43 // 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;
#41 = Utf8 java/lang/runtime/ObjectMethods
#42 = Utf8 bootstrap
#43 = 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;
#44 = String #11 // radius
#45 = MethodHandle 1:#7 // REF_getField example/Circle.radius:D
#46 = Utf8 InnerClasses
#47 = Class #48 // java/lang/invoke/MethodHandles$Lookup
#48 = Utf8 java/lang/invoke/MethodHandles$Lookup
#49 = Class #50 // java/lang/invoke/MethodHandles
#50 = Utf8 java/lang/invoke/MethodHandles
#51 = Utf8 Lookup
{
private final double radius;
descriptor: D
flags: (0x0012) ACC_PRIVATE, ACC_FINAL
example.Circle(double);
descriptor: (D)V
flags: (0x0000)
Code:
stack=3, locals=3, args_size=2
0: aload_0
1: invokespecial #1 // Method java/lang/Record."<init>":()V
4: aload_0
5: dload_1
6: putfield #7 // Field radius:D
9: return
LineNumberTable:
line 4: 0
MethodParameters:
Name Flags
radius
public final java.lang.String toString();
descriptor: ()Ljava/lang/String;
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokedynamic #13, 0 // InvokeDynamic #0:toString:(Lexample/Circle;)Ljava/lang/String;
6: areturn
LineNumberTable:
line 3: 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 #17, 0 // InvokeDynamic #0:hashCode:(Lexample/Circle;)I
6: ireturn
LineNumberTable:
line 3: 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 #21, 0 // InvokeDynamic #0:equals:(Lexample/Circle;Ljava/lang/Object;)Z
7: ireturn
LineNumberTable:
line 3: 0
public double radius();
descriptor: ()D
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #7 // Field radius:D
4: dreturn
LineNumberTable:
line 3: 0
}
SourceFile: "Circle.java"
Error: unknown attribute
Record: length = 0x8
00 01 00 0B 00 0C 00 00
BootstrapMethods:
0: #37 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 example/Circle
#44 radius
#45 REF_getField example/Circle.radius:D
InnerClasses:
public static final #51= #47 of #49; // Lookup=class java/lang/invoke/MethodHandles
总结一下这个代码清单告诉我们的有关Circle类规范化信息:
1. 正如我们讨论过的,Circle被定义为final,所以我们不能修改它的数据,也不能改变自动生成的方法以及不能继承子类。
2. 它从java.lang.Record继承而来。它具有一个数据,5个方法以及4个参数(attributes)。
数据:private final double radius;初始化值为0.0。
方法:public final String toString()、public final int hashCode()、public final boolean equals(Object)、 public final double radius()以及构造器public Circle(double)。
3. 这些方法由方法处理器(Method Handles)在调用时激活和援引(Invoke Dynamic)。
4. 一个启动方法ObjectMethod.bootstrap利用数据类中的数据名如radius和访问方法,如radius()来产生其他各个方法, 如toString, hashCode(), equals()以及Circle(double)。