JEP 500 是 JDK 26 中已正式交付的一项特性,它为 Java 生态做了一项关键准备:在未来的 JDK 版本中,通过深度反射(Deep Reflection)修改 final 字段的行为将默认被禁止。当前,你可以在 JDK 26 中看到相应的警告信息,并有充分的时间来识别和调整那些依赖此“灰色通道”的代码。

一、基本信息卡片

项目 内容
JEP 编号 500
标题 Prepare to Make Final Mean Final(为“让 final 回归本义”做准备)
负责人 Ron Pressler
所属版本 JDK 26
状态 Closed / Delivered(已关闭 / 已交付)
类型 Feature
范围 SE(Standard Edition)
组件 core-libs
创建时间 2025/02/06
更新时间 2026/01/21

二、背景与动机

2.1 final 的本义与现实的割裂

在 Java 语言中,final 关键字的设计初衷是表示不可变性。一旦 final 实例字段在构造函数中被赋值,或者 static final 字段在类初始化器中被赋值,它们就不应该再被更改。这种不可变性对于程序正确性的推理至关重要,同时也是 JVM 进行一系列激进优化的基础——例如常量折叠,它可以将常量表达式仅计算一次,从而显著提升性能。

然而,理想与现实之间存在着一条裂缝:Java 平台自身提供了多个 API,允许任何代码在任何时间修改 final 字段。其中最广为人知的就是深度反射 API,即通过 java.lang.reflect.FieldsetAccessible(true)set 方法。

下面这段代码直观地展示了这一矛盾:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.johnson.example;

/**
* 测试 final 字段
*
* @author johnson lin
* @date 2026/4/19 00:39
*/
public class FinalFieldExample {
// 一个普通的类,包含一个 final 字段
static class C {
final int x;

C() {
x = 100;
}
}

public static void main(String[] args) throws Exception {
// 1. 通过深度反射获取 C 中的 final 字段 x
java.lang.reflect.Field f = C.class.getDeclaredField("x");
// 让 C 的 final 字段变得可修改
f.setAccessible(true);

// 2. 创建一个 C 的实例
C obj = new C();
// 输出 100
System.out.println(obj.x);

// 3. 修改该对象中的 final 字段
f.set(obj, 200);
// 输出 200
System.out.println(obj.x);

f.set(obj, 300);
// 输出 300
System.out.println(obj.x);
}
}
/**
* 运行结果:
* 100
* 200
* 300
*/

原本被标记为 final 的字段,实际上和普通字段一样可以被随意篡改。这种局面让开发者无法信任任何 final 字段的值,也让 JVM 无法基于 final 的不可变性做出可靠的优化假设。

2.2 历史包袱:为何 final 能被修改?

这个看似“荒谬”的设计,源自一段历史权衡。自 JDK 5 起,final 字段在 Java 内存模型中扮演着重要角色——它们的不可变性是并发环境下对象安全初始化的基石。然而,final 的不可变性又与序列化的需求产生了冲突:序列化库在反序列化时需要直接向对象的字段赋值,即使是 final 字段也不例外。

为了支撑这一使用场景,JDK 5 中修改了反射 API,使其能够操作 final 字段。事后来看,这种不加约束的功能开放是一种糟糕的选择——它以牺牲完整性为代价,换取了一个特殊场景的便利。

此后,Java 平台开始逐步收紧这一通道:

  • JDK 15 引入隐藏类,JDK 16 引入记录类,它们都禁止通过深度反射修改 final 字段;
  • JDK 17 对 JDK 内部 API 进行了强封装;
  • JDK 24 开始移除 sun.misc.Unsafe 中可用于修改 final 字段的方法。

JEP 500 正是在“默认完整性”这一原则下的延续:final 字段在默认情况下真正不可变

三、JEP 500 的核心目标

JEP 500 的目标并非一步到位地彻底禁止修改 final 字段,而是为未来的强制约束做好准备:

  1. 为 Java 生态做好准备:在未来的某个 JDK 版本中,通过深度反射修改 final 字段的行为将默认被禁止。届时,应用开发者需要在启动时显式启用这一能力。
  2. 与记录类保持一致:让普通类中的 final 字段与记录类中隐式声明的字段行为一致,均不可被深度反射修改。
  3. 保障序列化库继续工作:允许序列化库在处理 Serializable 类时继续正常运作,即使是包含 final 字段的类也不例外。

非目标

  • 不计划弃用或移除任何 Java 平台 API。
  • 不计划阻止序列化库在反序列化过程中修改 final 字段。

四、核心变更详解

4.1 从 JDK 26 开始:警告而非阻止

在 JDK 26 中,通过深度反射修改 final 字段的行为仍然会成功,但 Java 运行时会默认发出一个警告。即使使用了 --add-opens 也无法消除这个警告。

这个警告的目的是让开发者在未来版本强制禁止这一行为之前,有足够的时间识别和修复问题代码。

使用 JDK 26 运行上文的代码示例,final 字段仍然会修改成功,但会发出警告信息,具体结果如下:

1
2
3
4
5
6
7
> D:\dev-env\jvms\store\26\bin\java.exe .\src\main\java\com\johnson\example\FinalFieldExample.java                                 
100
WARNING: Final field x in class com.johnson.example.FinalFieldExample$C has been mutated reflectively by class com.johnson.example.FinalFieldExample in unnamed module @351d0846 (file:/D:/src/core-java/core-java-26/./src/main/java/com/johnson/example/FinalFieldExample.java)
WARNING: Use --enable-final-field-mutation=ALL-UNNAMED to avoid a warning
WARNING: Mutating final fields will be blocked in a future release unless final field mutation is enabled
200
300

4.2 如何启用 final 字段修改能力?

如果你确实需要修改 final 字段(例如某些依赖注入、测试或模拟框架),可以通过命令行选项显式启用这一能力:

1
2
3
4
5
# 允许类路径上的所有代码修改任何 final 字段
java --enable-final-field-mutation=ALL-UNNAMED ...

# 允许指定模块修改 final 字段
java --enable-final-field-mutation=M1,M2 ...

例如:

1
2
3
4
> D:\dev-env\jvms\store\26\bin\java.exe --enable-final-field-mutation=ALL-UNNAMED .\src\main\java\com\johnson\example\FinalFieldExample.java                                                                                                                                                          
100
200
300

此外,你还可以通过以下方式传递该选项:

  • 设置环境变量 JDK_JAVA_OPTIONS
  • 在参数文件中指定,然后通过 java @config 启动;
  • 在可执行 JAR 文件的 MANIFEST.MF 中添加 Enable-Final-Field-Mutation: ALL-UNNAMED
  • 在使用 jlink 创建自定义运行时镜像时,通过 --add-options 选项嵌入。

注意:库开发者不应自行启用这一能力,而应告知其用户需要启用 final 字段修改。

4.3 控制违规行为的响应级别

JEP 500 引入了一个新的命令行选项 --illegal-final-field-mutation,用于控制当代码尝试违规修改 final 字段时,Java 运行时的反应。其设计风格与 JDK 9 引入的 --illegal-access 和 JDK 24 引入的 --illegal-native-access 一脉相承。

该选项支持以下四种模式:

模式 行为
allow 允许修改,不发出任何警告
warn 允许修改,但每个模块在首次违规时发出一次警告(JDK 26 的默认模式
debug 允许修改,但每次违规时都会发出警告和堆栈跟踪
deny 直接抛出 IllegalAccessException未来版本的默认模式

deny 成为默认模式后,allow 将被移除,但 warndebug 至少还会保留一个版本以供过渡。

4.4 警告信息示例

当代码尝试违规修改 final 字段时,默认会输出如下警告:

1
2
3
WARNING: Final field f in p.C has been [mutated/unreflected for mutation] by class com.foo.Bar.caller in module N (file:/path/to/foo.jar)
WARNING: Use --enable-final-field-mutation=N to avoid a warning
WARNING: Mutating final fields will be blocked in a future release unless final field mutation is enabled

每个模块最多只发出一次此类警告。

4.5 如何定位问题代码?

你可以通过以下两种方式精确定位哪些代码在修改 final 字段:

  1. 使用 debug 模式:启动时添加 --illegal-final-field-mutation=debug,每次违规都会输出完整的堆栈跟踪。
  2. 使用 JDK Flight Recorder:启用 JFR 后,JVM 会在代码修改 final 实例字段或使用 Lookup.unreflectSetter 获取 MethodHandle 时记录 jdk.FinalFieldMutation 事件。例如:
1
2
java -XX:StartFlightRecording:filename=recording.jfr ...
jfr print --events jdk.FinalFieldMutation recording.jfr

4.6 深度反射 API 的具体变化

在 JDK 26 中,Field::set 方法的行为发生了变化。当代码调用 f.set(...) 修改一个 final 字段时,需要满足以下条件才能成功:

  1. f.setAccessible(true) 已经成功执行;
  2. 该字段的声明类不在隐藏类中,也不是记录类;
  3. 该调用者所在的模块已启用 final 字段修改能力

条件 3 是 JDK 26 新增的。这意味着:即使 setAccessible(true) 成功了,如果调用者所在的模块没有启用 final 字段修改能力,调用 set(...) 时仍会抛出 IllegalAccessException(除非被 --illegal-final-field-mutation 抑制)。

相关 API 的变化包括:

  • MethodHandles.Lookup::unreflectSetter 的行为与 Field::set 同步变化;
  • Module::addOpens 方法不会为未启用 final 字段修改能力的模块开启修改 final 字段的权限。

4.7 序列化库的替代方案:sun.reflect.ReflectionFactory

JEP 500 并没有忘记序列化这一核心场景。对于需要反序列化包含 final 字段的类的库,官方推荐使用 sun.reflect.ReflectionFactory API。

这个 API 允许序列化库获得一个 MethodHandle,该句柄指向由 JDK 动态生成的代码,可以直接为实例字段赋值(包括 final 字段)。使用该 API 的序列化库无需启用 final 字段修改能力。

需要注意的是,ReflectionFactory 仅支持实现了 java.io.Serializable 接口的类。这一限制在开发者的序列化需求和广大开发者对正确、高效执行的需求之间取得了平衡:JVM 在进行优化时可以假设绝大多数普通类的 final 字段是永久不可变的,而对于 Serializable 类,则保留一定的灵活性。

4.8 对 clone 方法的建议

长期以来,包含 final 字段的类在实现 clone 方法时面临挑战。如果 clone 的实现调用了 super.clone(),就无法通过简单赋值来定制返回对象中 final 字段的值。一些实现会借助深度反射来绕过这一限制——但在未来的 JDK 版本中,这种做法将不再奏效。

Joshua Bloch 在 2001 年出版的《Effective Java》中就已经建议:尽量避免使用 clone,转而使用静态工厂方法。如果必须实现 clone,推荐将 super.clone() 替换为通过构造函数实例化的代码,这样构造函数可以直接将 final 字段初始化为所需的值,从而无需依赖深度反射。

4.9 原生代码(JNI)中的 final 字段修改

原生代码可以通过 JNI 的 Set<Type>FieldSetStatic<Type>Field 函数修改 Java 字段。在 final 字段上调用这些函数属于未定义行为——这意味着程序的完整性不再有保障,可能导致内存损坏或进程崩溃。

JEP 500 为 JNI 场景提供了新的诊断手段:

  • 如果以 -Xlog:jni=debug 启动,调用上述 JNI 函数修改 final 字段时会记录一条日志消息;
  • 如果以 -Xcheck:jni 启动,则会输出一条警告。

在未来版本中,JNI 中修改 final 字段的函数可能会被修改为“成功返回但不实际执行任何修改”。

五、兼容性与影响

自 JDK 5 起,修改 final 字段的能力就一直是 Java 平台的一部分,因此确实存在现有应用受到影响的风险。不过,JEP 500 的设计考虑到了充分的过渡期:开发者可以通过 --enable-final-field-mutation 显式启用这一能力,这与通过 --add-opens 禁用强封装的做法非常相似。

建议所有开发者:

  1. 尽快测试现有代码:在 JDK 26 上以 --illegal-final-field-mutation=debug 模式运行应用,全面排查哪些代码依赖 final 字段的修改;
  2. 评估是否真的需要修改 final 字段:对于大多数场景,应寻找架构上的替代方案,而非依赖这一“灰色通道”;
  3. 必要时显式启用:如果确实无法避免,可以在启动脚本中添加 --enable-final-field-mutation 选项。

六、小结与展望

JEP 500 是 Java 平台向“默认完整性”迈进的重要一步。它直面了自 JDK 5 以来悬而未决的设计遗憾——final 字段名义上不可变,实际上却可以被深度反射随意修改。这一矛盾不仅让程序正确性的推理变得困难,也阻碍了 JVM 进行更激进的优化。

在 JDK 26 中,JEP 500 采取了温和的过渡策略:先警告,后禁止。开发者有充足的时间来识别问题代码,并决定是寻找替代方案还是显式启用这一能力。

展望未来,当最终的限制全面生效时,final 将真正回归其本义——一个可靠的、不可变的承诺。这不仅会让 Java 程序更安全,也为其性能潜力释放出更大的空间。

参考文献

https://openjdk.org/jeps/500