Java final 关键字:从“不能改”到“安全发布”的深入理解
Java final 关键字:从“不能改”到“安全发布”的深入理解
前言
final 是 Java 里非常常见的关键字。它看起来简单:不能改、不能继承、不能重写。但真正写项目、看源码、准备面试时,会发现 final 不只是“加个限制”,它还和代码设计、对象不可变、线程安全、JVM 优化都有关系。
可以先用一个比喻理解它:final 像给某个位置贴上封条。封条贴在哪里,限制就发生在哪里。贴在变量上,变量不能换指向;贴在方法上,方法不能被重写;贴在类上,类不能被继承。
一、final 的核心定义
1.1 final 到底限制了什么
final 可以修饰:
- 变量:变量只能赋值一次。
- 成员变量:必须在对象构造完成前完成初始化。
- 静态变量:通常用于定义常量。
- 方法:子类不能重写。
- 类:不能被继承。
- 方法参数:方法内部不能重新给参数赋值。
一句话总结:
final限制的是“绑定关系”或“继承扩展点”,不是一定让对象本身不可变。
这句话很重要。比如:
1 | final List<String> list = new ArrayList<>(); |
final 锁住的是 list 这个变量和原始 ArrayList 对象之间的指向关系,不是锁住 ArrayList 内部的数据。
1.2 final 的三类常见用途
final 最常见的用途有三类:
- 表达不变:这个值不应该再次改变。
- 收紧扩展:这个类或方法不允许被继承或重写。
- 提升安全性:配合不可变对象、线程安全发布、常量设计使用。
在项目里,final 不是为了显得代码高级,而是为了把“不应该发生的变化”挡在编译期。
二、final 修饰变量
2.1 局部变量:只能赋值一次
局部变量被 final 修饰后,只能赋值一次。
1 | final int count = 10; |
这适合表达“这个中间结果后面不应该再改”。它像把便签贴在桌上:这张便签的内容确定后,不允许被擦掉重写。
示例:
1 | public List<String> mutableFinalReference() { |
这里 values 不能重新指向另一个集合,但集合内部仍然可以添加元素。
2.2 成员变量:对象构造完成前必须初始化
成员变量被 final 修饰后,必须在声明处、构造方法中或初始化代码块中完成赋值。
1 | private final String id; |
这类字段适合表示对象的核心身份。例如订单号、用户 ID、配置项创建时间等。
在对象建好之后,这些字段就不应该再变化。对象像一张身份证,身份证号从创建开始就固定。
2.3 空白 final:先声明,后初始化
只声明但不立刻赋值的 final 字段,叫空白 final。
1 | public static class BlankFinalExample { |
空白 final 的价值是:不同对象可以有不同初始值,但每个对象初始化后都不能再改。
2.4 static final:定义常量
static final 通常用于定义类级别常量。
在 Java 中:
static表示类级别,即这个变量属于类本身,而不是某个对象。所有对象共享同一个值。final表示不可变,即赋值后不能再修改。
1 | public static final int MAX_RETRY = 3; |
意思是:
- 这是类级别的常量;
- 任何地方都可以通过
ClassName.MAX_RETRY访问; - 它的值永远是 3,不能再改。
命名习惯是全大写,单词之间用下划线,例如:
1 | public static final int DEFAULT_TIMEOUT_SECONDS = 30; |
注意:只有基本类型和 String 字面量这类编译期常量,才可能被编译器内联到使用方字节码中。
2.4.1 编译期常量 vs 运行期常量
Java 编译器对 static final 常量有一个特殊优化:编译期常量会被直接写到使用方的字节码中,这叫内联(inlining)。
2.4.2 什么是编译期常量?
- 基本类型(int、long、double、boolean 等)或者 String 字面量。
- 值在编译时就能确定,例如:
1 | public static final int MAX_RETRY = 3; |
这里 3 和 "final-demo" 都是编译期就能确定的值。
2.4.3 什么不是编译期常量?
- 运行时才能确定的值,例如:
1 | public static final int RANDOM = new Random().nextInt(); |
这些值不能在编译时确定,因此不会被内联。
2.4.4 内联的后果
当你在一个类里定义:
1 | public static final int MAX_RETRY = 3; |
然后在另一个类使用:
1 | int retry = MyClass.MAX_RETRY; |
- 编译器会把
retry = 3;直接写到字节码里,而不是访问MyClass.MAX_RETRY。 - 这样在运行时,甚至不需要加载
MyClass的常量。

三、final 修饰引用类型
3.1 final 引用不等于对象不可变
这是 final 最容易被误解的地方。
1 | final StringBuilder builder = new StringBuilder("start"); |
final 只是不允许变量重新指向别的对象。至于原对象内部能不能变,要看对象自身是否可变。
测试代码:
1 | public static class FinalReferenceHolder { |
对应测试:
1 |
|
3.2 如何写出真正更安全的不可变对象
步骤:
- 类声明为
final。 - 所有字段
private final。 - 不提供 setter。
- 构造函数中防御性拷贝可变字段。
- Getter 返回只读视图或拷贝。
- 保证
this不在构造函数中逃逸。
示例:
1 | public final class Person { |
如果想让对象更接近不可变,只写 final 字段还不够,还要注意防御性拷贝。
1 | private final List<String> tags; |
这里做了两件事:
- 构造时复制外部传入的集合,防止外部继续修改原集合影响对象内部。
- 返回时包装成不可修改视图,防止调用方通过 getter 修改内部集合。
项目中常见场景:
- 配置对象。
- DTO 快照。
- 订单创建后的核心信息。
- 缓存 Key。
- 多线程共享的只读数据。

四、final 修饰方法
4.1 final 方法不能被重写
方法加上 final 后,子类不能重写这个方法。
1 | public static class ParentService { |
子类中如果写:
1 | // public String stableApi() { return "changed"; } |
会直接编译失败。
这适合用于稳定流程中的关键步骤。比如模板方法模式中,父类定义主流程,某些关键步骤不允许子类改写,避免破坏整体逻辑。
4.2 final 方法的项目使用场景
常见场景:
- 安全敏感方法:鉴权、签名校验、金额计算入口。
- 框架骨架方法:父类规定调用顺序,子类只能改扩展点。
- 不希望子类破坏语义的方法:例如对象身份、关键状态判断。
但也不要滥用。final 方法会减少扩展能力,如果项目需要通过继承做定制,就要慎重。
五、final 修饰类
5.1 final 类不能被继承
类加上 final 后,不能有子类。
1 | public static final class FinalUtility { |
典型代表是 String。String 被设计成 final,一个重要原因是它经常被用于常量池、HashMap Key、类加载、权限判断等基础场景。如果允许继承并篡改行为,整个 Java 生态都会变得不可靠。
5.2 final 类的项目使用场景
适合使用 final 类的情况:
- 工具类:只提供静态方法,不需要继承。
- 值对象:如 Money、Coordinate、DateRange。
- 安全敏感类:不希望子类改写行为。
- 语义已经完整的类:没有必要开放继承。
不适合使用 final 类的情况:
- 需要框架代理的类,例如某些 AOP、ORM 场景。
- 明确作为父类提供扩展点的类。
- 单元测试需要通过继承替身的旧代码。
六、final 修饰参数和 lambda 捕获
6.1 final 参数:防止方法内部改引用
1 | public String appendSuffix(final String value) { |
final 参数常用于表达“这个入参只读”。不过现代 Java 项目里并不是所有参数都必须加 final,否则代码会显得啰嗦。更推荐在关键方法、复杂方法或者团队规范要求时使用。
6.2 effectively final:Java 8 lambda 的隐形 final
Java 8 开始,lambda 可以捕获局部变量,但这个变量必须是 final 或 effectively final。
1 | public Runnable effectivelyFinalLocalVariable() { |
message 没有显式写 final,但它赋值后没有再变,所以是 effectively final。
为什么要这样限制?
因为局部变量存在线程栈里,而 lambda 对象可能在方法返回后才执行。Java 不能直接让 lambda 持有那个栈变量本身,只能捕获它的值。为了避免“捕获时一个值,执行时又变了”的混乱,Java 要求它必须事实上不变。
七、final 与底层原理
7.1 编译期检查
final 的很多限制都发生在编译期。
例如:
- final 变量重复赋值,编译失败。
- final 方法被重写,编译失败。
- final 类被继承,编译失败。
- lambda 捕获非 effectively final 变量,编译失败。
这说明 final 很多时候不是运行时防御,而是把错误提前到编译阶段。
7.2 编译期常量和常量内联
对于这种代码:
1 | public static final int MAX_RETRY = 3; |
如果其他类使用了 MAX_RETRY,编译器可能直接把 3 写进使用方字节码里。这叫常量内联。
这带来一个项目里的真实问题:如果一个公共 jar 修改了常量值,但调用方没有重新编译,调用方可能还在使用旧值。
所以公共 SDK 里的常量设计要谨慎,尤其是跨模块、跨服务发布时。
7.3 final 字段与 Java 内存模型
final 字段在 Java 内存模型里有特殊语义。
简单说:如果对象构造过程没有发生 this 逃逸,那么构造完成后,其他线程看到这个对象时,能更可靠地看到 final 字段的正确初始化值。
这叫 final 字段的安全发布语义。
例子:
1 | private final String id; |
id 在构造方法中完成赋值。只要构造期间不把 this 暴露出去,其他线程拿到对象后,更不容易看到 id 还是默认值 null 的异常情况。
这也是不可变对象天然更适合多线程共享的原因之一。
7.4 final 和性能优化
早期有人会说:给方法加 final,JVM 就能内联,性能更好。
这句话不能完全当作现代 Java 的主要理由。现代 JIT 编译器会根据运行时类型信息做内联优化,即使方法不是 final,也可能被优化。final 更重要的价值是表达设计意图和限制错误扩展。
也就是说:
不要为了微小性能收益滥用 final,要为了清晰语义和正确边界使用 final。
八、项目中如何使用 final
8.1 推荐使用 final 的场景
建议使用:
- 常量:
public static final。 - 构造后不变的字段:如 ID、创建时间、配置项。
- 工具类:
final class + private constructor。 - 不可变值对象。
- 不允许子类改写的关键方法。
- lambda 捕获变量本身需要保持不变的场景。
示例:
1 | public final class Money { |
这个类一看就表达了:金额对象创建后不应该被随意修改。
8.2 不建议滥用 final 的场景
不建议:
- 所有局部变量都机械加 final。
- 所有类都加 final,导致扩展和测试困难。
- 框架需要生成代理的类上随意加 final。
- 误以为 final 集合就是不可变集合。
尤其在 Spring AOP、Mockito、Hibernate 等框架场景下,final 类和 final 方法可能影响代理、增强或 mock。是否能支持取决于框架机制和配置,不能一概而论。
九、面试如何考察 final
9.1 高频基础题
常见问题:
final可以修饰什么?final、finally、finalize有什么区别?- final 变量和普通变量有什么区别?
- final 引用对象还能修改内容吗?
- final 方法和 final 类分别有什么作用?
简洁回答:
final是关键字,用于限制赋值、继承、重写。finally是异常处理结构中的代码块,通常用于释放资源。finalize是 Object 的方法,曾用于对象回收前回调,但不推荐使用,并且新版本 Java 已经逐步废弃。
9.2 进阶面试题
可能继续追问:
private final List<String> list是否线程安全?- 如何设计一个不可变类?
static final int和static final Integer在编译期常量上有什么区别?- lambda 为什么只能捕获 final 或 effectively final 变量?
- final 字段在 Java 内存模型里有什么特殊语义?
回答思路:
- final 引用不代表对象不可变,更不直接等于线程安全。
- 不可变类需要 final 类、private final 字段、不提供 setter、防御性拷贝、不要泄露可变内部对象。
- 基本类型和 String 字面量的
static final才是典型编译期常量。 - lambda 捕获的是局部变量的值,不允许后续变化是为了避免语义混乱。
- final 字段有安全发布语义,但前提是构造期间不能让
this逃逸。
总结
final 的表层含义是“不能改”,深层价值是“明确边界”。
它可以帮助我们把一些错误提前到编译期:不该重复赋值的变量、不该重写的方法、不该继承的类,都能被更早拦住。
但要记住:final 不是万能不可变药水。final 引用不能换对象,不代表对象内部不能变;final 字段有安全发布语义,也不代表整个对象天然线程安全。
项目里真正好的用法,是把 final 用在能表达设计意图的位置:常量、不可变值对象、稳定 API、关键流程、只读配置。这样代码不仅能跑,还能告诉后来的人:这里的变化边界已经想清楚了。