面试官问:final、finally、finalize有什么区别?(附图解+避坑指南)
📝 摘要:final是修饰符(类不可继承/方法不可重写/变量不可变),finally是异常处理中保证执行的代码块,finalize是Object类中被废弃的GC钩子方法。本文用“三种最终”比喻+JVM底层原理+替代方案(try-with-resources/Cleaner)+安全视角(对象复活风险)+JDK版本演进(9→17→21),彻底讲透这道面试必考题。
💬 面试还原
面试官:final、finally、finalize 有什么区别?
这道题堪称面试界的“大家来找茬”——三个词长得像,但实际毫无关系。很多新手会把它们混淆,如果只回答“final是常量、finally是异常、finalize是垃圾回收”这种表面答案,面试官会立刻追问“finally一定执行吗”“finalize为什么被废弃”“能不能用别的替代”。
今天用一张图 + 一个比喻 + 三道追问,让你彻底拿下这道题。
一句话总结:final是编译期的“不变”约束,finally是运行时的“兜底”保障,finalize是JDK 9就已废弃的GC“告别”仪式。
🧠 final、finally、finalize一图看懂

🍵 生活比喻:三种“最终”
想象一个家族企业的传承场景:

-
final = 独生子女证
这孩子就是最终版,不能再有弟弟妹妹继承家业(类不可继承),家传武功也不能乱改(方法不可重写),名字写进族谱就不能改(变量不可变)。 -
finally = 追悼会
无论生前关系好不好(有没有异常),追悼会都一定会办。除非家族直接破产注销了(System.exit(0)),否则该办还得办。 -
finalize = 临终遗言
人走了之后(GC回收前),说几句遗言。但什么时候说不知道(时机不确定),说了也不一定有人听(不保证执行),而且现在已经不提倡搞这套了(JDK9废弃)。
📊 关键对比表
| 维度 | final | finally | finalize() |
|---|---|---|---|
| 类型 | 关键字/修饰符 | 关键字 | Object类的方法 |
| 作用 | 定义“不可变性” | 异常处理后必须执行的代码块 | 对象被GC回收前的回调钩子 |
| 修饰类 | 类不能被继承 | — | — |
| 修饰方法 | 方法不能被重写 | — | — |
| 修饰变量 | 变量值/引用不可变 | — | — |
| 执行保证 | 编译期保证 | 运行时保证(除System.exit(0)外) | 不保证执行 |
| 版本状态 | ✅ 正常使用 | ✅ 正常使用 | ❌ JDK 9 废弃 → JDK 17 弱化 → JDK 21 彻底移除 |
🔍 面试官追问(重点!)
追问1:finally 一定执行吗?有什么特殊情况?
回答要点:大部分情况下会执行,但有两个“死穴”。
详细回答:
finally 块在大多数情况下一定会执行,但有两种特殊情况不会:
System.exit(0):主动终止 JVM 进程,finally 不会执行。- JVM 崩溃或线程被杀死:如物理断电、
kill -9强制杀进程等。另外,如果在 finally 块中抛出异常或执行
return,也会影响最终行为。不要在 finally 中写return,否则会覆盖 try 中的返回值,导致难以排查的 Bug。
追问2:如果在 try 块中写了 return,finally 还会执行吗?
答:
会执行。finally 块中的代码会在
return语句执行之后、方法真正返回给调用者之前执行。这也是为什么不要在 finally 中写return——它会覆盖 try 的返回值。
追问3:finalize() 为什么被废弃?替代方案是什么?(⭐ 核心加分题)
回答要点:性能差 + 时机不确定 + 安全漏洞 + 有更好的替代方案。
详细回答:
finalize() 被废弃的主要原因:
- 执行时机不确定:GC 何时调用 finalize() 完全由 JVM 决定,不能依赖它来释放关键资源。
- 性能差:GC 调用 finalize() 会拖慢垃圾回收效率,延长对象生命周期。
- 可能造成对象“复活”:在 finalize() 中重新将
this赋值给外部引用,对象就“复活”了,导致 GC 白干。- 安全隐患:恶意代码可以通过重写 finalize() 来复活包含敏感信息(如密码)的对象,导致敏感数据一直驻留在内存中,造成信息泄露。
JDK 版本演进:
- JDK 9:标记
@Deprecated废弃- JDK 17:弱化支持,推荐使用替代方案
- JDK 21:彻底移除(实际在 JDK 18 已标记
forRemoval=true)替代方案:
- try-with-resources(推荐):实现
AutoCloseable接口,自动释放资源。关闭顺序为 LIFO(后进先出),与构造顺序相反。- 显式调用
close():手动释放资源,最常见方式。java.lang.ref.Cleaner(JDK 9+):基于PhantomReference(虚引用) 实现,比 finalize 更轻量且不阻塞 GC。
🔥 Cleaner 实战:NIO 直接内存清理
Cleaner 最典型的应用场景是 NIO 的直接内存(Direct Memory)。直接内存不受 JVM GC 管控,必须显式释放,这恰好是 Cleaner 的核心应用场景。
💡 源码关联:JDK 底层
ByteBuffer.allocateDirect()创建的直接缓冲区,内部封装Cleaner实现堆外内存自动释放,这是Cleaner最核心、最广泛的落地场景。
Cleaner vs finalize 核心区别:finalize 依赖 JVM 的 GC 周期,执行时机不可控;Cleaner 通过 PhantomReference 将清理动作与引用队列关联,可以在对象变得不可达时及时且异步地执行清理,不阻塞 GC 线程,性能更优且不会被“对象复活”问题干扰。
import java.lang.ref.Cleaner;
// 模拟一个需要被清理的 Native 资源
class NativeResource implements Runnable {
private final String name;
public NativeResource(String name) {
this.name = name;
System.out.println(name + " 已分配 Native 内存");
}
@Override
public void run() {
// 实际场景:释放 malloc() 分配的内存
System.out.println(name + " 已释放 Native 内存 (Cleaner 触发)");
}
}
public class CleanerDemo {
private static final Cleaner CLEANER = Cleaner.create();
public static void main(String[] args) throws InterruptedException {
NativeResource resource = new NativeResource("DirectBuffer");
// 注册清理动作:当 resource 对象不可达时,自动执行 resource.run()
CLEANER.register(resource, resource);
resource = null; // 解除强引用
System.gc(); // 建议 JVM 执行 GC(不保证立即执行)
Thread.sleep(500); // 若 GC 未及时触发,可适当延长休眠时间
System.out.println("主程序结束");
}
}
⚠️ Cleaner 生产规范:
Cleaner清理动作是单线程执行的,大量资源清理场景可能造成清理线程阻塞。业务层主动close()始终是首选,Cleaner仅作为防御性兜底,不可替代显式资源释放。
💣 常见坑点
坑1:final 修饰引用类型 ≠ 对象不可变
final List<String> list = new ArrayList<>();
list.add("hello"); // ✅ 合法!list 的引用没变,但对象内容变了
list = new ArrayList<>(); // ❌ 编译错误!引用不能重新赋值
关键:final 修饰引用类型时,锁住的是引用地址,不是对象内容。
坑2:finally 中写 return 覆盖 try 的返回值
public int test() {
try {
return 1;
} finally {
return 2; // <--- 致命陷阱!这会覆盖 try 中的 return 1
}
}
结果:返回 2,而不是 1。这是生产环境难以排查的 Bug 源头。
坑3:finalize 中抛出异常导致对象无法回收
@Override
protected void finalize() throws Throwable {
throw new RuntimeException(); // <--- 异常被忽略,对象无法及时回收
}
正确做法:永远不要依赖 finalize,也不要在其中抛出未捕获的异常。
坑3:finalize 对象复活演示(理解原理)
// finalize 对象复活演示(JDK9+已废弃,仅用于理解原理)
static class Resurrectable {
static Resurrectable saved;
@Override
protected void finalize() throws Throwable {
System.out.println("finalize 执行,对象复活!");
saved = this; // 重新赋值给静态引用,对象“复活”
super.finalize();
}
}
// 使用
Resurrectable obj = new Resurrectable();
obj = null;
System.gc(); // 第一次 GC → 对象复活
Thread.sleep(100);
Resurrectable.saved = null;
System.gc(); // 第二次 GC → 才真正回收
关键:对象复活后,GC 需要第二次才能将其真正回收,这是 finalize 导致资源延迟释放的典型例子。
坑4:try-finally 关闭多个资源时的异常掩盖
// ❌ 旧方式:第一个 close 的异常会掩盖第二个
FileInputStream fis = null;
try {
fis = new FileInputStream("test.txt");
} finally {
if (fis != null) fis.close(); // 如果 close 抛异常,try 中的异常被吞掉
}
// ✅ 新方式:try-with-resources 自动处理
try (FileInputStream fis = new FileInputStream("test.txt")) {
// 使用 fis
} // 多个资源按反序关闭,异常被自动 Suppressed
底层本质:try-with-resources 是 Java 7 引入的语法糖,编译后仍然生成 try-finally 结构,多个资源按 LIFO 顺序关闭。异常抑制通过 Throwable.addSuppressed() 实现,因此主异常不会丢失。
💻 可运行验证代码
public class FinalFinallyFinalizeDemo {
// ===== final 演示 =====
static class Parent {
final void cannotOverride() {
System.out.println("父类的 final 方法");
}
}
// class Child extends Parent {
// void cannotOverride() { } // ❌ 编译错误!不能重写 final 方法
// }
// ===== finally 演示 =====
public static int testFinally() {
try {
System.out.println("try 执行");
return 1;
} finally {
System.out.println("finally 执行");
// return 2; // <--- 如果取消注释,返回值变成 2
}
}
public static void testFinallyExit() {
try {
System.out.println("try 执行");
System.exit(0); // JVM 退出
} finally {
System.out.println("finally 执行"); // ❌ 不会执行!
}
}
// ===== finalize 演示(已废弃,仅用于演示) =====
static class Resource {
@Override
@Deprecated
protected void finalize() throws Throwable {
System.out.println("finalize() 被调用(不推荐使用)");
super.finalize();
}
}
public static void main(String[] args) {
// 1. finally 与 return 的执行顺序
System.out.println("testFinally() 返回值: " + testFinally());
// 2. finalize 演示
Resource r = new Resource();
r = null;
System.gc(); // 建议 JVM 执行 GC(不保证立即执行)
System.out.println("main 结束");
}
}
输出示例:
try 执行
finally 执行
testFinally() 返回值: 1
main 结束
❓ 评论区挑战
问题:下面代码的输出是什么?为什么?
public class FinallyTest {
public static int test() {
try {
return 10;
} finally {
return 20;
}
}
public static void main(String[] args) {
System.out.println(test());
}
}
A. 10
B. 20
C. 编译报错
D. 运行时异常
欢迎在评论区写出你的答案和理由。我会在下一篇文章中发布后更新该文章,公布答案及错误选项逐项解析。
📌 总结
| 特性 | 一句话概括 | 核心作用 | 版本状态 |
|---|---|---|---|
| final | 编译期的“不变”约束 | 类不可继承、方法不可重写、变量不可变 | ✅ 正常使用 |
| finally | 运行时的“兜底”保障 | 异常处理后必定执行(除 System.exit 外) | ✅ 正常使用 |
| finalize() | 已废弃的 GC“告别”仪式 | 对象回收前的清理钩子 | ❌ JDK9 废弃 → JDK17 弱化 → JDK21 彻底移除 |
面试官最看重的四个点:
- 三者本质完全不同:final是修饰符,finally是关键字,finalize是方法
- finally的“死穴”:
System.exit(0)和 JVM 崩溃- finalize已废弃:替代方案是 try-with-resources / Cleaner
- 安全加分项:finalize 可能被恶意代码利用导致对象“复活”和信息泄露
📚 系列导航
- 上一篇:面试官问:String、StringBuilder、StringBuffer有什么区别?
- 下一篇预告:面试官问:接口和抽象类有什么区别?
- 全部85题目录:点击查看
📘 搭配学习效果更佳
本篇图解帮你快速建立知识画面记忆,如果想深入理解源码实现和实战避坑细节,可以配合姊妹系列 《Java 100天进阶之路》 对应章节一起学:
从零基础到上岗就业,108篇完整学习地图,每篇标配 生活类比 + 可运行代码 + 避坑表 + 面试高频题 + 练习题,不背八股文,真正讲透“为什么”。
学习建议:图解系列负责“快速建立知识图谱”,进阶系列负责“深入理解原理”,两个系列搭配使用,面试备考效率翻倍。
💬 ① 你在实际开发中遇到过 finally 中写 return 导致的诡异 Bug 吗?
💬 ② 你项目中使用过Cleaner或ByteBuffer.allocateDirect管理直接内存吗?欢迎评论区分享你的故事。
2万+

被折叠的 条评论
为什么被折叠?



