文章目录
-
- 前言
- [一、final 的核心定义](#一、final 的核心定义)
-
- [1.1 final 到底限制了什么](#1.1 final 到底限制了什么)
- [1.2 final 的三类常见用途](#1.2 final 的三类常见用途)
- [二、final 修饰变量](#二、final 修饰变量)
-
- [2.1 局部变量:只能赋值一次](#2.1 局部变量:只能赋值一次)
- [2.2 成员变量:对象构造完成前必须初始化](#2.2 成员变量:对象构造完成前必须初始化)
- [2.3 空白 final:先声明,后初始化](#2.3 空白 final:先声明,后初始化)
- [2.4 static final:定义常量](#2.4 static final:定义常量)
- [三、final 修饰引用类型](#三、final 修饰引用类型)
-
- [3.1 final 引用不等于对象不可变](#3.1 final 引用不等于对象不可变)
- [3.2 如何写出真正更安全的不可变对象](#3.2 如何写出真正更安全的不可变对象)
- [四、final 修饰方法](#四、final 修饰方法)
-
- [4.1 final 方法不能被重写](#4.1 final 方法不能被重写)
- [4.2 final 方法的项目使用场景](#4.2 final 方法的项目使用场景)
- [五、final 修饰类](#五、final 修饰类)
-
- [5.1 final 类不能被继承](#5.1 final 类不能被继承)
- [5.2 final 类的项目使用场景](#5.2 final 类的项目使用场景)
- [六、final 修饰参数和 lambda 捕获](#六、final 修饰参数和 lambda 捕获)
-
- [6.1 final 参数:防止方法内部改引用](#6.1 final 参数:防止方法内部改引用)
- [6.2 effectively final:Java 8 lambda 的隐形 final](#6.2 effectively final:Java 8 lambda 的隐形 final)
- [七、final 与底层原理](#七、final 与底层原理)
-
- [7.1 编译期检查](#7.1 编译期检查)
- [7.2 编译期常量和常量内联](#7.2 编译期常量和常量内联)
- [7.3 final 字段与 Java 内存模型](#7.3 final 字段与 Java 内存模型)
- [7.4 final 和性能优化](#7.4 final 和性能优化)
- [八、项目中如何使用 final](#八、项目中如何使用 final)
-
- [8.1 推荐使用 final 的场景](#8.1 推荐使用 final 的场景)
- [8.2 不建议滥用 final 的场景](#8.2 不建议滥用 final 的场景)
- [九、面试如何考察 final](#九、面试如何考察 final)
-
- [9.1 高频基础题](#9.1 高频基础题)
- [9.2 进阶面试题](#9.2 进阶面试题)
- 总结
前言
final 是 Java 里非常常见的关键字。它看起来简单:不能改、不能继承、不能重写。但真正写项目、看源码、准备面试时,会发现 final 不只是"加个限制",它还和代码设计、对象不可变、线程安全、JVM 优化都有关系。
可以先用一个比喻理解它:final 像给某个位置贴上封条。封条贴在哪里,限制就发生在哪里。贴在变量上,变量不能换指向;贴在方法上,方法不能被重写;贴在类上,类不能被继承。
一、final 的核心定义
1.1 final 到底限制了什么
final 可以修饰:
- 变量:变量只能赋值一次。
- 成员变量:必须在对象构造完成前完成初始化。
- 静态变量:通常用于定义常量。
- 方法:子类不能重写。
- 类:不能被继承。
- 方法参数:方法内部不能重新给参数赋值。
一句话总结:
final限制的是"绑定关系"或"继承扩展点",不是一定让对象本身不可变。
这句话很重要。比如:
java
final List<String> list = new ArrayList<>();
list.add("A"); // 可以
// list = new ArrayList<>(); // 不可以
final 锁住的是 list 这个变量和原始 ArrayList 对象之间的指向关系,不是锁住 ArrayList 内部的数据。
1.2 final 的三类常见用途
final 最常见的用途有三类:
- 表达不变:这个值不应该再次改变。
- 收紧扩展:这个类或方法不允许被继承或重写。
- 提升安全性:配合不可变对象、线程安全发布、常量设计使用。
在项目里,final 不是为了显得代码高级,而是为了把"不应该发生的变化"挡在编译期。
二、final 修饰变量
2.1 局部变量:只能赋值一次
局部变量被 final 修饰后,只能赋值一次。
java
final int count = 10;
// count = 20; // 编译错误
这适合表达"这个中间结果后面不应该再改"。它像把便签贴在桌上:这张便签的内容确定后,不允许被擦掉重写。
测试代码位置:
text
src/main/java/com/likerhood/design/finalkeyword/FinalKeywordDemo.java
src/test/java/com/likerhood/design/finalkeyword/FinalKeywordDemoTest.java
示例:
java
public List<String> mutableFinalReference() {
final List<String> values = new ArrayList<>();
values.add("A");
values.add("B");
// values = new ArrayList<>(); // 编译错误
return values;
}
这里 values 不能重新指向另一个集合,但集合内部仍然可以添加元素。
2.2 成员变量:对象构造完成前必须初始化
成员变量被 final 修饰后,必须在声明处、构造方法中或初始化代码块中完成赋值。
java
private final String id;
public FinalKeywordDemo(String id, List<String> tags) {
this.id = id;
}
这类字段适合表示对象的核心身份。例如订单号、用户 ID、配置项创建时间等。
在对象建好之后,这些字段就不应该再变化。对象像一张身份证,身份证号从创建开始就固定。
2.3 空白 final:先声明,后初始化
只声明但不立刻赋值的 final 字段,叫空白 final。
java
public static class BlankFinalExample {
private final String name;
public BlankFinalExample(String name) {
this.name = name;
}
}
空白 final 的价值是:不同对象可以有不同初始值,但每个对象初始化后都不能再改。
2.4 static final:定义常量
static final 通常用于定义类级别常量。
java
public static final int MAX_RETRY = 3;
public static final String APP_NAME = "final-demo";
命名习惯是全大写,单词之间用下划线,例如:
java
public static final int DEFAULT_TIMEOUT_SECONDS = 30;
注意:只有基本类型和 String 字面量这类编译期常量,才可能被编译器内联到使用方字节码中。也就是说,其他类引用 MAX_RETRY 时,编译后的代码可能直接写死为 3。这也是公共常量修改后,有时需要重新编译调用方的原因。
三、final 修饰引用类型
3.1 final 引用不等于对象不可变
这是 final 最容易被误解的地方。
java
final StringBuilder builder = new StringBuilder("start");
builder.append("-A"); // 可以
// builder = new StringBuilder(); // 不可以
final 只是不允许变量重新指向别的对象。至于原对象内部能不能变,要看对象自身是否可变。
测试代码:
java
public static class FinalReferenceHolder {
private final StringBuilder builder = new StringBuilder("start");
public String append(String value) {
builder.append(value);
return builder.toString();
}
}
对应测试:
java
@Test
public void test_finalReferenceIsNotImmutableObject() {
FinalKeywordDemo.FinalReferenceHolder holder = new FinalKeywordDemo.FinalReferenceHolder();
assertEquals("start-A", holder.append("-A"));
assertEquals("start-A-B", holder.append("-B"));
}
3.2 如何写出真正更安全的不可变对象
如果想让对象更接近不可变,只写 final 字段还不够,还要注意防御性拷贝。
java
private final List<String> tags;
public FinalKeywordDemo(String id, List<String> tags) {
this.tags = new ArrayList<>(tags);
}
public List<String> getTags() {
return Collections.unmodifiableList(tags);
}
这里做了两件事:
- 构造时复制外部传入的集合,防止外部继续修改原集合影响对象内部。
- 返回时包装成不可修改视图,防止调用方通过 getter 修改内部集合。
项目中常见场景:
- 配置对象。
- DTO 快照。
- 订单创建后的核心信息。
- 缓存 Key。
- 多线程共享的只读数据。
四、final 修饰方法
4.1 final 方法不能被重写
方法加上 final 后,子类不能重写这个方法。
java
public static class ParentService {
public final String stableApi() {
return "stable-api";
}
}
子类中如果写:
java
// public String stableApi() { return "changed"; }
会直接编译失败。
这适合用于稳定流程中的关键步骤。比如模板方法模式中,父类定义主流程,某些关键步骤不允许子类改写,避免破坏整体逻辑。
4.2 final 方法的项目使用场景
常见场景:
- 安全敏感方法:鉴权、签名校验、金额计算入口。
- 框架骨架方法:父类规定调用顺序,子类只能改扩展点。
- 不希望子类破坏语义的方法:例如对象身份、关键状态判断。
但也不要滥用。final 方法会减少扩展能力,如果项目需要通过继承做定制,就要慎重。
五、final 修饰类
5.1 final 类不能被继承
类加上 final 后,不能有子类。
java
public static final class FinalUtility {
private FinalUtility() {
}
public static String normalize(String value) {
return value == null ? "" : value.trim().toLowerCase();
}
}
典型代表是 String。String 被设计成 final,一个重要原因是它经常被用于常量池、HashMap Key、类加载、权限判断等基础场景。如果允许继承并篡改行为,整个 Java 生态都会变得不可靠。
5.2 final 类的项目使用场景
适合使用 final 类的情况:
- 工具类:只提供静态方法,不需要继承。
- 值对象:如 Money、Coordinate、DateRange。
- 安全敏感类:不希望子类改写行为。
- 语义已经完整的类:没有必要开放继承。
不适合使用 final 类的情况:
- 需要框架代理的类,例如某些 AOP、ORM 场景。
- 明确作为父类提供扩展点的类。
- 单元测试需要通过继承替身的旧代码。
六、final 修饰参数和 lambda 捕获
6.1 final 参数:防止方法内部改引用
java
public String appendSuffix(final String value) {
// value = "changed"; // 编译错误
return value + "-suffix";
}
final 参数常用于表达"这个入参只读"。不过现代 Java 项目里并不是所有参数都必须加 final,否则代码会显得啰嗦。更推荐在关键方法、复杂方法或者团队规范要求时使用。
6.2 effectively final:Java 8 lambda 的隐形 final
Java 8 开始,lambda 可以捕获局部变量,但这个变量必须是 final 或 effectively final。
java
public Runnable effectivelyFinalLocalVariable() {
String message = "hello";
return () -> System.out.println(message);
}
message 没有显式写 final,但它赋值后没有再变,所以是 effectively final。
为什么要这样限制?
因为局部变量存在线程栈里,而 lambda 对象可能在方法返回后才执行。Java 不能直接让 lambda 持有那个栈变量本身,只能捕获它的值。为了避免"捕获时一个值,执行时又变了"的混乱,Java 要求它必须事实上不变。
七、final 与底层原理
7.1 编译期检查
final 的很多限制都发生在编译期。
例如:
- final 变量重复赋值,编译失败。
- final 方法被重写,编译失败。
- final 类被继承,编译失败。
- lambda 捕获非 effectively final 变量,编译失败。
这说明 final 很多时候不是运行时防御,而是把错误提前到编译阶段。
7.2 编译期常量和常量内联
对于这种代码:
java
public static final int MAX_RETRY = 3;
如果其他类使用了 MAX_RETRY,编译器可能直接把 3 写进使用方字节码里。这叫常量内联。
这带来一个项目里的真实问题:如果一个公共 jar 修改了常量值,但调用方没有重新编译,调用方可能还在使用旧值。
所以公共 SDK 里的常量设计要谨慎,尤其是跨模块、跨服务发布时。
7.3 final 字段与 Java 内存模型
final 字段在 Java 内存模型里有特殊语义。
简单说:如果对象构造过程没有发生 this 逃逸,那么构造完成后,其他线程看到这个对象时,能更可靠地看到 final 字段的正确初始化值。
这叫 final 字段的安全发布语义。
例子:
java
private final String id;
public FinalKeywordDemo(String id, List<String> tags) {
this.id = 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 捕获变量本身需要保持不变的场景。
示例:
java
public final class Money {
private final long cents;
private final String currency;
public Money(long cents, String currency) {
this.cents = cents;
this.currency = currency;
}
}
这个类一看就表达了:金额对象创建后不应该被随意修改。
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、关键流程、只读配置。这样代码不仅能跑,还能告诉后来的人:这里的变化边界已经想清楚了。