bigywater.tech
Java 内部原理 · 40

Record 类真实反编译:equals/hashCode/toString 是一条 invokedynamic,不是手写代码

Record(Java 16 正式发布)号称能自动生成 equals/hashCode/toString。真实验证这些方法到底是不是真的按字段值工作,再反编译看看编译器真实把这些方法编译成了什么样子。

p1.equals(p2) → true
两个字段值相同、但引用不同的 Point 实例,equals() 真实按字段值判定相等,不是引用比较
一个方法体 = 一条 invokedynamic
真实反编译发现 equals/hashCode/toString 在字节码里各自只有一条 invokedynamic 指令,没有手写的比较逻辑

上一篇: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 的字段真实是 private final 的

反射直接确认字段的可见性和可变性。

真实实测
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 从设计上就是不可变数据载体。

真实验证:javap 揭示的实现方式——运行时生成,不是编译期手写

反编译 Point 这个 record 类,看 equals/hashCode/toString 真实是怎么实现的。

真实实测 · javap -c 反编译片段
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 文件里。

为什么要这样设计?这个机制真实带来的好处是:record 的字段列表信息(哪些字段参与 equals/hashCode/toString)被保留在常量池的引导方法参数里,而不是被"编译死"成具体的比较代码——这给了 JDK 未来优化这些生成逻辑的空间(比如运行时根据实际情况选择更快的哈希算法),不需要重新编译所有用到 record 的代码。真实代价是:第一次调用这些方法时,会有一次额外的引导方法链接开销,之后的调用会被真实缓存下来,不会每次都重新生成。

分步演示:一行 record 声明背后真实发生了什么

每一行都是真实运行这段代码得到的结果。

record 自动生成的 toString/equals/hashCode/访问器,加上紧凑构造器校验,真实全部由编译器和 JVM 元数据支撑
这一步新验证的行为

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,record 语法自 Java 16 起正式可用。
    • 和字符串拼接编译产物那篇一样,都是"真实反编译发现现代 Java 大量使用 invokedynamic 而不是编译期手写代码"这个趋势下的真实案例。
    • 没有涉及:record 实现接口、record 的静态字段和静态方法、密封接口(sealed interface)配合 record 做穷尽性模式匹配——这些留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号