Record(Java 16 正式发布)号称能自动生成 equals/hashCode/toString。真实验证这些方法到底是不是真的按字段值工作,再反编译看看编译器真实把这些方法编译成了什么样子。
上一篇:Comparator 违反契约:真实触发 JDK 自带的 Comparison method violates its general contract
定义一个带紧凑构造函数(compact constructor)的 record,在赋值前插入校验逻辑,顺便验证这一点也真实生效。
RecordDemo.java record Point(int x, int y) { Point { // 紧凑构造函数:不用重复写参数列表 if (x < 0 || y < 0) { throw new IllegalArgumentException("坐标真实不允许为负数: (" + x + ", " + y + ")"); } } }
p1 == p2 (引用比较) = false p1.equals(p2) (真实按字段值比较) = true p1.hashCode() == p2.hashCode() = true p1.equals(p3) = false(字段值不同,真实不相等) p1.toString() 真实结果 = Point[x=3, y=4] p1.x() = 3, p1.y() = 4 真实抛出 IllegalArgumentException: 坐标真实不允许为负数: (-1, 2)
真实结果符合预期:p1 和 p2 是两个独立创建的实例(== 为 false),但字段值相同,equals() 真实判定为相等,hashCode() 也真实相等;toString() 自动生成的格式是 类名[字段名=值, ...];访问字段用的是 x()/y() 这种和字段同名的方法,不是老式 JavaBean 的 getX();紧凑构造函数里的校验逻辑也真实生效,负数坐标真实构造失败。
反射直接确认字段的可见性和可变性。
record 内部字段 x 的修饰符 = private final Point.class.isRecord() = true Point 的父类 = class java.lang.Record RecordComponents 真实反射出的组件名 = x : int y : int
每个 record 真实都隐式继承自 java.lang.Record 这个抽象类(所以 record 不能再继承其它类,这是它和普通 class 的一个真实限制),字段真实是 private final 的,没有任何办法从外部直接改写——record 从设计上就是不可变数据载体。
反编译 Point 这个 record 类,看 equals/hashCode/toString 真实是怎么实现的。
public final java.lang.String toString();
Code:
0: aload_0
1: invokedynamic #25, 0 // InvokeDynamic #1:toString:(LPoint;)Ljava/lang/String;
6: areturn
public final int hashCode();
Code:
0: aload_0
1: invokedynamic #29, 0 // InvokeDynamic #1:hashCode:(LPoint;)I
6: ireturn
真实反编译结果很意外:toString()、hashCode()、equals() 三个方法体里,真实都只有一条 invokedynamic 指令,没有任何手写的字段比较、字符串拼接代码。这和系列前面 字符串拼接 那篇看到的 invokedynamic 机制是同一套底层设施——真正的比较/哈希/格式化逻辑,由 JDK 内部一个叫 ObjectMethods 的引导方法(bootstrap method)在类第一次真正调用这些方法时,根据 record 的字段列表动态生成出来,而不是 javac 在编译期就把逻辑手写进 .class 文件里。
每一行都是真实运行这段代码得到的结果。