阅读时间:约 45 分钟 难度:中级 → 高级 前置知识:Java 基础语法、异常处理、JVM 基础
一、三个"final"的前世今生——先看清它们是谁
很多 Java 初学者都会被这三个长得像的词搞晕。它们的名字里都有 "final",但职责完全不同——用一句话概括:
| 关键字 | 作用域 | 本质 |
|---|---|---|
| final | 类、方法、变量 | "不可变/不可继承" —— 编译期的约束 |
| finally | try-catch 块 | "无论如何都要执行" —— 异常处理的保障 |
| finalize | Object 方法 | "GC 前的最后挣扎" —— 已被废弃的资源回收钩子 |
它们唯一的共同点是:都表达了一种"终结"或"最终"的语义。 final 表示"到此为止,不能再改了";finally 表示"无论如何,最后都要跑这段";finalize 表示"对象临死前,让你看最后一眼"。
下面我们逐一深入。

二、final 关键字——编译期的"封条"
2.1 final 修饰类:不允许被继承
public final class String { ... } // JDK 中最著名的 final 类
当类被 final 修饰时,任何尝试继承它的代码都会在编译期被拒绝。JDK 中 String、Integer、Long 等包装类都是 final 的。
设计意图:
- 安全性:防止子类破坏父类的不可变性契约。
String为 final 才能保证它在整个 JVM 中都是不可变的,否则恶意子类可以重写length()返回假值。 - 性能:JIT 编译器可以对 final 类的方法调用进行去虚拟化(devirtualization),直接将虚方法调用转化为静态绑定,省去方法表的查找开销。
- 设计完整性:明确告知使用者"这个类的行为已经完备,不需要也不应该被扩展"。
底层实现:在 class 文件的 access_flags 中设置 ACC_FINAL 标志位(0x0010),类加载验证阶段会检查此标志。
2.2 final 修饰方法:不允许被子类重写
public class Parent {
public final void criticalLogic() { ... }
}
设计意图:
- 保护核心算法:模板方法模式中,
final方法确保骨架流程不被破坏。 - 安全关键路径:防止子类在安全检查、加密解密等关键方法上"做手脚"。
- 性能优化:与 final 类同理,JIT 可以对 final 方法进行内联优化。
值得注意的细节:
- final 方法可以被子类继承和调用,只是不能重写。
private方法隐式是 final 的(因为 private 方法对子类不可见,根本不存在"重写"的前提)。static方法不能被重写(只能被子类"隐藏"),加 final 主要是为了阻止子类定义同签名静态方法。
2.3 final 修饰变量:赋值一次且仅一次
这是 final 最复杂、也是面试中最喜欢深挖的部分。分为四种场景:
2.3.1 final 成员变量(实例字段)
public class Example {
// 方式一:声明时赋值
private final int a = 10;
// 方式二:构造代码块赋值
private final int b;
{ b = 20; }
// 方式三:构造函数中赋值(最常用)
private final int c;
public Example(int c) {
this.c = c;
}
// 以上三种方式必须选一种,否则编译报错
}

JVM 对 final 成员变量有特殊的**写屏障(StoreStore barrier)**保证:构造函数中对 final 字段的写入,一定在对象引用对外可见之前完成。这就是 JSR-133 中著名的 final 语义:
// 在没有 final 语义前,其他线程可能看到未初始化的字段值
class SafePublication {
private final int x;
public SafePublication(int x) { this.x = x; }
// 通过 final 语义,构造函数完成后,所有线程看到的 x 一定是初始化后的值
}

2.3.2 final 静态变量(类字段)
// 编译时常量:用字面量或常量表达式初始化
public static final int MAX = 100; // 会被内联,编译后变成 100
// 运行时确定:在静态初始化块中赋值
public static final int VALUE;
static { VALUE = compute(); }
// 区别:MAX 在编译期就替换成 100,甚至改源码不重新编译依赖类也不会变
// VALUE 的值在类加载时确定
2.3.3 final 局部变量
void foo(final int x) { // 参数也可以 final
final int y = 10;
// y = 20; // 编译错误
}
final 局部变量的关键用途是让匿名内部类或 lambda 能够捕获它。在 Java 8 之前,内部类只能访问 final 局部变量;Java 8 放宽为 effectively final(虽然没有显式声明 final,但实际上没被重新赋值)。
2.3.4 final 引用 vs final 对象——面试高频陷阱
final StringBuilder sb = new StringBuilder("hello");
sb.append(" world"); // ✅ 合法,sb 指向的对象可以被修改
// sb = new StringBuilder(); // ❌ 不合法,sb 引用本身不可变

核心认知:final 修饰的是引用本身,而不是引用指向的对象。final 不等于"不可变(immutable)"。要实现真正的不可变对象,需要:
- 类本身用 final 修饰
- 所有字段都是 private final
- 不提供任何修改状态的方法
- 如果字段是引用类型,返回时做防御性拷贝
2.4 final 的底层实现——JVM 视角
当 JVM 遇到 final 修饰的字段时:
- 编译期常量(static final + 基本类型/字符串):值被直接写入常量池,使用时直接内联,不产生字段访问指令。这就是为什么只改一个常量类不重新编译所有依赖会导致值不更新的根因。
- 实例 final 字段:在
<init>方法(构造函数)末尾插入StoreStore内存屏障,确保:- final 字段的写入不会被重排到构造函数返回之后
- 对象引用的发布不会被重排到 final 字段写入之前
- 方法/类的 final:JIT 编译器通过
CHA(Class Hierarchy Analysis)检测,如果某个类/方法是 final 的(或者当前加载的类中没有子类/重写),就执行内联优化。
三、finally 块——异常处理的"守门人"
3.1 finally 的执行保证
try {
doSomething(); // 可能抛出异常
} catch (Exception e) {
handle(e);
} finally {
cleanup(); // 无论如何都会执行
}
finally 的执行规则:
- try 正常结束 → 执行 finally
- try 中抛出异常 → catch 处理后执行 finally
- try 中 return → 先执行 finally,再 return
- catch 中 return → 先执行 finally,再 return
- catch 中也抛异常 → 执行 finally 后,向外抛出新异常
3.2 唯一不执行 finally 的情况
// 情况一:System.exit(0)
try {
System.exit(0);
} finally {
System.out.println("永远不会打印"); // JVM 进程直接终止
}
// 情况二:Runtime.halt()
try {
Runtime.getRuntime().halt(0); // 比 exit 更暴力,连 shutdown hook 都不执行
} finally { ... }
// 情况三:守护线程中所有非守护线程结束
Thread daemon = new Thread(() -> {
try {
while (true) { Thread.sleep(1000); }
} finally {
// 如果此时 main 线程结束,JVM 退出,守护线程直接被杀,finally 不执行
}
});
daemon.setDaemon(true);
// 情况四:操作系统级别的 kill -9
// 情况五:try/catch 块中发生无限循环或死锁

面试常问:try { return 1; } finally { return 2; } 返回值是多少?答案是 2,因为 finally 中的 return 会覆盖 try 中的返回值。这是生产代码中应该避免的写法。
3.3 finally 与 return 的微妙交互
public static int test() {
int x = 1;
try {
return x; // ① 计算返回值(x=1),暂存到局部变量槽
} finally {
x = 2; // ② 修改局部变量 x=2
// return x; // ③ 如果取消注释,返回 2
}
// ④ 返回步骤①暂存的值 1
}
// 返回 1,不是 2!
底层原理:return x 在字节码层面的操作是——先将 x 的值加载到操作数栈,然后跳转到 finally 块。finally 执行完后,操作数栈顶的值就是最终的返回值。所以修改局部变量 x 不影响已经压栈的返回值。
但如果返回值是引用类型,情况就不同了:
public static StringBuilder test() {
StringBuilder sb = new StringBuilder("hello");
try {
return sb; // 压栈的是 sb 的引用
} finally {
sb.append(" world"); // 通过引用修改了对象内容
}
}
// 返回的 StringBuilder 内容是 "hello world"
3.4 try-with-resources:finally 的进化版
Java 7 引入的 try-with-resources 本质上是对 finally 模式的语法糖封装:
// 传统写法
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("file.txt"));
// ...
} finally {
if (br != null) br.close();
}
// Java 7+ 写法(资源自动关闭)
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
// ...
}
// 编译器生成的字节码中,finally 仍然存在,但由编译器自动生成
另一个关键特性是 suppressed exception:如果 try 体和 close 都抛出异常,try-with-resources 会将 close 的异常作为 suppressed exception 附加到 try 的异常上,可以通过 ex.getSuppressed() 获取。而手动 finally 写法通常会丢失其中一个异常信息。

四、finalize 方法——Java 历史上最大的设计失误之一
4.1 finalize 的工作机制
public class Resource {
@Override
protected void finalize() throws Throwable {
try {
// 释放资源
closeNativeHandle();
} finally {
super.finalize(); // 必须调用父类的 finalize
}
}
}
finalize 的执行链路:
- GC 发现对象不可达
- 如果对象重写了 finalize 且未被调用过,对象被放入
Finalizer队列(java.lang.ref.Finalizer$FinalizerThread) - Finalizer 线程(守护线程,优先级较低)逐个调用
finalize()方法 - 下一次 GC 时,对象才真正被回收

4.2 为什么 finalize 被彻底废弃
Java 9 标记为 @Deprecated,Java 18 标记为 @Deprecated(forRemoval=true):
问题一:调用时机不可预测 finalize 的执行时间完全由 GC 和 Finalizer 线程决定。在极端情况下——尤其是对象多、GC 压力大时——finalize 可能延迟数分钟甚至永远不会执行。一个持有文件描述符或数据库连接的对象如果依赖 finalize 释放资源,可能导致资源耗尽。
问题二:性能灾难 每次回收重写了 finalize 的对象需要至少两次 GC。第一次将对象从不可达变为 finalize 队列中,第二次才能真正回收。更糟糕的是,Finalizer 线程的优先级较低,大量 finalize 积压会拖垮整体性能。
问题三:对象复活——安全噩梦
public class Zombie {
private static Zombie zombie;
@Override
protected void finalize() {
zombie = this; // 对象"复活"了!重新变成可达
}
}
// 这个对象永远不会被 GC 回收,但它的构造函数已经不会再被调用了
// 它处于一种"活死人"状态——不能用正常途径访问,却占用内存
问题四:异常被吞没 finalize 中抛出的异常会被 Finalizer 线程静默忽略,不会输出到任何地方。
问题五:没有执行顺序保证 多个对象的 finalize 方法可能以任意顺序执行,甚至并发执行。这意味着如果对象 A 的 finalize 依赖对象 B 的资源,而 B 的 finalize 先执行了,A 的 finalize 就会操作已释放的资源。
4.3 Cleaner:现代化替代方案
Java 9 引入的 java.lang.ref.Cleaner 是对 finalize 的替代:
public class ModernResource implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
private final Cleaner.Cleanable cleanable;
private final NativeResource nativeResource;
public ModernResource() {
this.nativeResource = allocateNative();
// 注册清理动作——当 ModernResource 对象变成幻像可达时执行
this.cleanable = cleaner.register(this, () -> freeNative(nativeResource));
}
@Override
public void close() {
cleanable.clean(); // 主动释放,Cleaner 保证只执行一次
}
}
Cleaner 相比 finalize 的改进:
- 基于
**PhantomReference**:对象的清理不会导致对象复活 - 清理线程独立:不阻塞 GC
- 保证只执行一次:多次调用
clean()不会重复执行清理动作 - 严格禁止访问被清理对象:清理动作中的
this已经被置为不可达 - 更好的异常处理:抛出异常不会静默吞没
但 Cleaner 仍然只是安全网,不能替代显式的资源释放。 最佳实践仍然是实现 AutoCloseable 并在 close() 中主动释放资源,Cleaner 仅作兜底。

五、横向对比——一眼看穿三个 final
| 维度 | final | finally | finalize/Cleaner |
|---|---|---|---|
| 分类 | 修饰符/关键字 | 异常处理结构 | 对象生命周期方法 |
| 作用时机 | 编译期约束 + JVM 优化 | 运行时异常处理 | GC 回收时(不确定) |
| 是否常用 | 极其常用 | 常用(但逐渐被 try-with-resources 替代) | finalize 废弃,Cleaner 少用 |
| 性能影响 | 正面(帮助 JIT 优化) | 无显著影响 | finalize 严重拖慢 GC |
| 可预测性 | 完全确定 | 完全确定(除极端情况) | 高度不确定 |
| 推荐程度 | ✅ 推荐 | ✅ 适度使用 | ❌ 不推荐 finalize,Cleaner 仅兜底 |
六、生产实践与常见陷阱
6.1 final 的使用建议
// ✅ 好的实践
public final class ImmutableConfig { ... } // 不可变类必须 final
public void process(final Order order) { ... } // 明确参数不可变意图
public static final int DEFAULT_TIMEOUT = 3000; // 命名常量
// ⚠️ 需要注意的场景
// 1. final 和序列化——final 字段在反序列化时需要特殊处理
public class Cache implements Serializable {
private final Map<String, Object> cache; // 反序列化时,构造函数不会被调用
// 需要通过 readObject() 或其他方式处理
}
// 2. final 字段的延迟初始化
public class LazyInit {
private final Helper helper;
// 如果 Helper 可能为 null,final 就不合适;考虑用 Optional 或 @Nullable
}
// 3. 匿名内部类的"闭包"限制(Java 8 前)
void process() {
final int count = 10;
new Thread(new Runnable() {
public void run() { System.out.println(count); }
}).start();
}
// Java 8+ 可以用 effectively final 替代
6.2 finally 的使用建议
// ✅ try-with-resources 优先(用于 AutoCloseable 资源)
try (Connection conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
// ...
}
// ✅ finally 用于非 Closeable 的清理工作
Lock lock = new ReentrantLock();
lock.lock();
try {
// ...
} finally {
lock.unlock();
}
// ❌ 避免 finally 中的 return/throw
// 这会导致 try 中抛出的异常或 return 值被覆盖
try {
return computeResult();
} finally {
// return -1; // 千万不要这样做!
// throw new RuntimeException(); // 同样危险
}
// ❌ 不要在 finally 中执行耗时操作
// ❌ 不要在 finally 中吞掉异常
6.3 资源释放的正确姿势
// 终极资源管理模式
public class DatabaseResource implements AutoCloseable {
private Connection conn;
private static final Cleaner cleaner = Cleaner.create();
private final Cleaner.Cleanable cleanable;
public DatabaseResource() {
this.conn = createConnection();
this.cleanable = cleaner.register(this, () -> {
// Cleaner 兜底 —— 不应该依赖它,只是一个安全网
System.err.println("WARNING: Resource not explicitly closed!");
silentClose(conn);
});
}
@Override
public void close() {
cleanable.clean(); // 取消 Cleaner 注册
conn.close(); // 主动释放
}
}
核心原则:
- 永远不要在 finalize 中释放资源——它可能永远不执行
- Cleaner 只能作为安全网——不能替代显式的 close()
- 实现 AutoCloseable + try-with-resources——这是 Java 最推荐的方式
- 在 close() 中调用 cleaner.clean()——取消兜底逻辑,避免重复释放
七、总结
| 知识点 | 一句话总结 |
|---|---|
| final 类 | 防止继承,保护设计完整性,助 JIT 内联 |
| final 方法 | 防止重写,保护核心逻辑,开启静态绑定 |
| final 变量 | 引用不可变,不保证对象不可变;JSR-133 保证构造安全发布 |
| finally 块 | try-catch 的保障机制,倾向用 try-with-resources 替代 |
| finalize | Java 史上最大的坑之一,已被废弃,绝不要用 |
| Cleaner | finalize 的现代化替代,基于 PhantomReference,仅做安全兜底 |
🎯 面试问答模块
Q1:**final**、**finally**、**finalize**有什么区别?
面试官视角:这是 Java 面试的经典入门题,但不是让你背八股——我希望看到你不仅知道区别,还能说出各自的底层机制和使用场景。
参考答案:final 是编译期约束——修饰类的目的是阻止继承并帮助 JVM 做去虚拟化优化,修饰方法防止子类重写破坏模板方法模式的骨架,修饰变量保证引用不可变(但要区分引用不可变和对象不可变)。finally 是异常处理的保障机制——无论 try 中如何返回或抛异常,finally 一定执行(唯一的例外是 System.exit/守护线程被杀/OS kill)。finalize 是 JDK 历史上最大的设计失误之一,不可预测、拖性能、能复活对象、吞异常,已被 @Deprecated;替代方案是 Cleaner(基于 PhantomReference,只兜底不依赖)。
Q2:**String**类为什么设计为 final?如果 String 不是 final 会发生什么?
面试官视角:这道题考察的是你对不可变性和安全性的深入理解,以及能否从攻击者角度思考问题。
参考答案:从三个层面回答。第一,安全性:如果 String 可以被继承和重写,恶意代码可以创建一个 MutableString 子类,交给安全框架做权限校验时拿到一个值,执行时又变成另一个值——TOCTOU(Time-of-check-time-of-use)攻击。第二,性能:String 不可变才使字符串常量池和 hashCode 缓存(hash 字段是 int,被 lazy 计算后缓存在对象里)成为可能。第三,线程安全:不可变对象天然线程安全,String 不必加任何同步手段就能在多线程间安全共享。如果 String 不是 final,所有基于 String 的安全模型(类加载、反射、安全管理器)都需要重新设计。
Q3:**try { return 1; } finally { return 2; }**返回什么?如果在 finally 中修改了返回值会怎样?
面试官视角:这题表面考语法,实际考的是你对字节码执行模型的理解。我需要听到操作数栈这个词。
参考答案:返回 2。finally 中的 return 会覆盖 try 中的 return。底层原因是:return x 在字节码层面是先把 x 的值加载到操作数栈顶,然后跳转到 finally 块;finally 执行完后返回栈顶的值。如果 finally 中有 return,新的值被压栈覆盖。特别要注意的是:如果 x 是基本类型,finally 中修改局部变量 x 不影响栈顶的返回值(执行 x = 2 之后返回的仍是原来的 1);如果是引用类型,虽然引用本身不变,但可以通过引用修改对象的内容。
Q4:final 变量在 JVM 层面有什么特殊的并发保证?
面试官视角:这题区分普通 Java 工程师和技术专家——你不仅要会用 final,还要理解 JSR-133 内存模型对 final 字段的特殊语义。
参考答案:JSR-133(Java 5 内存模型)为 final 字段引入了专门的语义保证:构造函数中对 final 字段的写入,以及通过 final 字段能到达的所有变量的写入,在构造函数正常完成后,对所有线程可见。具体实现是通过在构造函数返回前插入 StoreStore 内存屏障——禁止 final 字段的写入被重排到构造函数返回之后,禁止对象引用的发布被重排到 final 字段写入之前。这就让不可变对象(所有字段都是 final)可以实现安全的无同步发布。但要注意:这要求构造函数中不能让 this 逃逸(比如在构造函数中启动线程或注册监听器),否则 final 语义失效。
Q5:为什么不应该在 finalize 中释放数据库连接?Cleaner 能解决所有问题吗?
面试官视角:考察你对资源管理痛点的真实理解,以及能否做出合理的工程权衡。
参考答案:不在 finalize 中释放连接的原因:① 执行时间不可预测——连接可能在池子里被长期占用,依赖 finalize 会导致连接池提前耗尽;② 需要至少两次 GC,在此期间连接对象及其底层 socket 一直占用资源;③ Finalizer 线程优先级低,大量 finalize 积压会导致内存压力;④ 如果 finalize 中 close() 抛异常,异常被吞,连接泄漏但没有人知道。Cleaner 解决了一部分问题(基于 PhantomReference 不需两次 GC、独立的清理线程、只执行一次),但它仍然不能替代显式的 close():HBase 等中间件曾因过度依赖 Cleaner 导致堆外内存耗尽——Cleaner 的执行时机依然由 GC 决定,而 GC 只关注堆内存压力,对堆外(DirectByteBuffer 的 native memory)无感知。正确的做法永远是实现 AutoCloseable + try-with-resources,Cleaner 只是兜底。
Q6:设计一个不可能修改的配置类,你会怎么用 final?
面试官视角:从理论到实践的跨越——把多个知识点串联成一个工程方案。
参考答案:
public final class AppConfig { // ① 类 final:防止子类破坏
private final String dbHost; // ② 所有字段 private final
private final int dbPort;
private final List<String> allowedIPs; // ③ 引用类型也要不可变
public AppConfig(String dbHost, int dbPort, List<String> allowedIPs) {
this.dbHost = dbHost;
this.dbPort = dbPort;
// ④ 防御性拷贝:防止外部修改传入的 List
this.allowedIPs = List.copyOf(allowedIPs);
}
public List<String> getAllowedIPs() {
return allowedIPs; // ⑤ 直接返回,List.copyOf 的结果是不可变的
// 如果是普通集合,这里要 return Collections.unmodifiableList(...)
}
}
关键在于:final 修饰类本身 + 所有字段 private final + 引用类型做防御性拷贝或使用不可变集合 + 不提供任何 setter。这样构造出来的对象通过 JSR-133 的 final 语义保证,可以在多线程中安全共享而无需同步。
📌 延伸思考:除了本文讨论的三个 "final",Java 中还有哪些看似相似但原理迥异的概念组合?比如
List、Set、Map接口的继承关系?==和equals?throw和throws?建议阅读本系列 003|接口与抽象类 和 005|equals 与 hashCode 契约,它们和本文共同构成了 Java 基础中最容易混淆的知识网络。