Valhalla 项目首次预览:JEP 401 重新定义 Java 对象的 == 运算符
JEP 401(值对象预览版)已经集成到了 JDK 28 中。该提案引入了仅包含 final 字段、没有身份标识的类实例,并更改了这些对象中 == 运算符的行为,为更扁平、无需内存分配的 JVM 表示形式铺平了道路。该预览功能默认处于禁用状态,可以在编译时和运行时通过 --enable-preview 参数启用。
对于开发者而言,其直接影响包括:新增的 value 修饰符、更严格的构造规则、同步限制,以及若干基于值的 JDK 类造成的迁移影响。该提案是在 OpenJDK 的 valhalla-dev 邮件列表上经过多年讨论后形成的。
使用 value 修饰符声明的类即为值类;其余类仍为标识类。其实例字段默认是 final 的,而且必须在观测新实例之前为每个字段赋值。
复制代码value class Point {private int x; // 隐式 finalprivate int y;public Point(int x, int y) {this.x = x; // 在对象构造完成之前对所有字段赋值this.y = y;}public int x() { return x; }public int y() { return y; }}
记录也可以使用修饰符,例如 value record Color(byte red, byte green, byte blue) { }。值类的实例方法不能用 synchronized 修饰。JEP 539(JVM 中的严格字段初始化)提供了用于强制执行构造规则的字节码验证机制。
语义变化最明显的是 == 运算符。对于身份对象,其行为保持不变。对于值对象,当两个操作数都是同一类的实例且字段值相同时,该运算就会返回成功;引用类型的字段则使用 == 进行递归比较。
复制代码Point p1 = new Point(3, 4);Point p2 = new Point(3, 4);assert p1 == p2; // true: 同一个类,字段值相通Object o1 = p1, o2 = p2;assert o1 == o2; // true: 仍然无法作为对象进行区分String s1 = "hamburger";String s2 = new String(s1);assert s1 != s2; // true: String 仍然是一个身份类
这并非鼓励放弃 equals 方法。JEP 401 并未将 == 重新定义为 equals 的替代方案;通常建议使用 equals 来比较对象,该建议仍然适用,因为值对象的内部状态并不总是与其所表示的状态一致。
启用该预览功能后,包括基本类型封装类和 LocalDate 在内的若干基于值的 JDK 类将变为值类。禁用预览功能时,编译器将继续使用其身份对象形式,而且行为仍然与 JDK 27 保持一致。编译时启用了预览功能的代码,运行时也必须保持预览功能的启用状态。
建议对已迁移的类进行重新编译,因为类文件属性 LoadableDescriptors 会在加载时告知 JVM 该类是一个值类。部分 API 将无法正常工作:为值对象创建 Reference 会抛出 IdentityException。而且,在 JDK 28 中,javac 将其身份警告扩展到了值类。
该功能的预期收益是性能优化,而非有保证的基准测试结果。JVM 可能会将值对象标量化为其组成字段,或将其扁平化为紧凑的表示形式,直接存储在字段或数组元素中。扁平化仍然受原子性限制;在传统平台上,可用的编码空间可能只有 64 位(包括一个空标志)。当这两种优化均不适用时,JVM 将回退到普通的内存分配机制,尤其是在优化后的 JIT 代码可用之前的预热阶段。
JEP 401 将这一问题归结为开发者意图与 Java 保障之间的脱节。表示复数、像素颜色或日期的简单类用于承载数据,而普通的 Java 对象却由身份来定义:每次调用构造函数都会生成一个可以与其他所有对象区分开来的对象。该 JEP 认为,对于简单类型而言,身份往往无关紧要,甚至可能带来负面影响。
身份还会带来运行时开销。它通常要求 JVM 为每个对象分配内存,并在每次使用时解引用该内存位置,这增加了垃圾回收器的压力并降低了局部性。逃逸分析可以回收部分开销,但其优化效果难以预测,而且一旦对象逃逸(包括被存储在字段或数组中时),优化便不再起作用。
该 JEP 承认,值对象对 Java 的对象模型带来了重大改变。开发人员可能会对 == 和 synchronized 的新行为感到惊讶,不过该提案预计,这类干扰相对不会很多,而且可控。if_acmpeq 字节码还新增了一项值对象检查,其中身份比较的情况仍将延续快速路径。
该文档指出了两项安全方面的注意事项:== 运算符和 identityHashCode 方法可能会间接泄露私有字段的值,而比较两个大型值对象树需要的时间可能无限长。
原文链接:https://www.infoq.com/news/2026/08/jep401-value-objects-preview/