Java 26 新特性:JEP 500 限制反射修改 final 字段——让 final 真正不可变

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.Field 的 setAccessible(true) 和 set 方法。
下面这段代码直观地展示了这一矛盾:
1 | package com.johnson.example; |
原本被标记为 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 字段,而是为未来的强制约束做好准备:
- 为 Java 生态做好准备:在未来的某个 JDK 版本中,通过深度反射修改
final字段的行为将默认被禁止。届时,应用开发者需要在启动时显式启用这一能力。 - 与记录类保持一致:让普通类中的
final字段与记录类中隐式声明的字段行为一致,均不可被深度反射修改。 - 保障序列化库继续工作:允许序列化库在处理
Serializable类时继续正常运作,即使是包含final字段的类也不例外。
非目标
- 不计划弃用或移除任何 Java 平台 API。
- 不计划阻止序列化库在反序列化过程中修改
final字段。
四、核心变更详解
4.1 从 JDK 26 开始:警告而非阻止
在 JDK 26 中,通过深度反射修改 final 字段的行为仍然会成功,但 Java 运行时会默认发出一个警告。即使使用了 --add-opens 也无法消除这个警告。
这个警告的目的是让开发者在未来版本强制禁止这一行为之前,有足够的时间识别和修复问题代码。
使用 JDK 26 运行上文的代码示例,final 字段仍然会修改成功,但会发出警告信息,具体结果如下:
1 | > D:\dev-env\jvms\store\26\bin\java.exe .\src\main\java\com\johnson\example\FinalFieldExample.java |
4.2 如何启用 final 字段修改能力?
如果你确实需要修改 final 字段(例如某些依赖注入、测试或模拟框架),可以通过命令行选项显式启用这一能力:
1 | # 允许类路径上的所有代码修改任何 final 字段 |
例如:
1 | > D:\dev-env\jvms\store\26\bin\java.exe --enable-final-field-mutation=ALL-UNNAMED .\src\main\java\com\johnson\example\FinalFieldExample.java |
此外,你还可以通过以下方式传递该选项:
- 设置环境变量
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 将被移除,但 warn 和 debug 至少还会保留一个版本以供过渡。
4.4 警告信息示例
当代码尝试违规修改 final 字段时,默认会输出如下警告:
1 | 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) |
每个模块最多只发出一次此类警告。
4.5 如何定位问题代码?
你可以通过以下两种方式精确定位哪些代码在修改 final 字段:
- 使用
debug模式:启动时添加--illegal-final-field-mutation=debug,每次违规都会输出完整的堆栈跟踪。 - 使用 JDK Flight Recorder:启用 JFR 后,JVM 会在代码修改
final实例字段或使用Lookup.unreflectSetter获取MethodHandle时记录jdk.FinalFieldMutation事件。例如:
1 | java -XX:StartFlightRecording:filename=recording.jfr ... |
4.6 深度反射 API 的具体变化
在 JDK 26 中,Field::set 方法的行为发生了变化。当代码调用 f.set(...) 修改一个 final 字段时,需要满足以下条件才能成功:
f.setAccessible(true)已经成功执行;- 该字段的声明类不在隐藏类中,也不是记录类;
- 该调用者所在的模块已启用
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>Field 和 SetStatic<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 禁用强封装的做法非常相似。
建议所有开发者:
- 尽快测试现有代码:在 JDK 26 上以
--illegal-final-field-mutation=debug模式运行应用,全面排查哪些代码依赖final字段的修改; - 评估是否真的需要修改
final字段:对于大多数场景,应寻找架构上的替代方案,而非依赖这一“灰色通道”; - 必要时显式启用:如果确实无法避免,可以在启动脚本中添加
--enable-final-field-mutation选项。
六、小结与展望
JEP 500 是 Java 平台向“默认完整性”迈进的重要一步。它直面了自 JDK 5 以来悬而未决的设计遗憾——final 字段名义上不可变,实际上却可以被深度反射随意修改。这一矛盾不仅让程序正确性的推理变得困难,也阻碍了 JVM 进行更激进的优化。
在 JDK 26 中,JEP 500 采取了温和的过渡策略:先警告,后禁止。开发者有充足的时间来识别问题代码,并决定是寻找替代方案还是显式启用这一能力。
展望未来,当最终的限制全面生效时,final 将真正回归其本义——一个可靠的、不可变的承诺。这不仅会让 Java 程序更安全,也为其性能潜力释放出更大的空间。
参考文献


