背景与动机
final、finally、finalize 名字很像,初学者和面试场景里经常被放在一起问,但它们完全不是同一类东西:
final是关键字,用来限制“能不能改”finally是异常处理语法块,用来保证“收尾动作会执行”finalize是Object里的方法,和垃圾回收有关,但早已不推荐依赖
如果只记“三个 final 开头”,很容易混。这篇笔记把它们放在一张对照表里,再分别拆开看。
核心对比
| 对比项 | final | finally | finalize |
|---|---|---|---|
| 类型 | 关键字 | 异常处理语法块 | Object 实例方法 |
| 主要作用 | 限制变量、方法、类被修改 | 无论是否异常,都执行收尾逻辑 | 对象被 GC 回收前做清理(理论上的钩子) |
| 出现位置 | 变量、方法、类声明处 | 必须配合 try 或 try-catch | 类中重写,由 JVM 在特定时机调用 |
| 是否必须手写 | 按需使用 | 按需使用 | 一般不应再依赖 |
| 现代 Java 建议 | 常量、不可继承类、不可重写方法时正常使用 | 资源释放优先用 try-with-resources | 已废弃,避免使用 |

记忆口诀可以记成:
final管“定不变”finally管“必收尾”finalize管“回收前” —— 但这条在现代 Java 里已经过时
final:限制修改
final 修饰的目标不同,含义也不同:
| 修饰目标 | 含义 |
|---|---|
| 变量 | 只能赋值一次,之后不能重新指向新值 |
| 方法 | 子类不能重写这个方法 |
| 类 | 这个类不能被继承 |
final class Config { final String env;
Config(String env) { this.env = env; }
final void printEnv() { System.out.println(env); }}上面这段里:
final class Config:别人不能再写class DevConfig extends Configfinal String env:构造完成后不能再给env重新赋值final void printEnv():子类不能重写printEnv()
注意:final 修饰引用类型时,限制的是“引用不能改”,不是“对象内部状态不能改”。
final java.util.List<String> names = new java.util.ArrayList<>();names.add("java"); // 可以names = new java.util.ArrayList<>(); // 编译错误更完整的用法见 Java 封装、继承与多态 和 Java 变量与基本数据类型。
finally:异常处理里的收尾块
finally 必须出现在 try 后面,用来放无论是否发生异常都要执行的逻辑,常见是关闭资源、写收尾日志。
java.io.BufferedReader reader = null;try { reader = java.nio.file.Files.newBufferedReader(java.nio.file.Path.of("a.txt")); System.out.println(reader.readLine());} catch (java.io.IOException e) { System.out.println("read fail: " + e.getMessage());} finally { if (reader != null) { try { reader.close(); } catch (java.io.IOException e) { e.printStackTrace(); } }}几个边界要记住:
finally不是“处理异常”,而是“做收尾”- 通常会在
return之前执行,所以不建议在finally里写复杂返回逻辑 - 如果 JVM 直接退出、进程崩溃,
finally也可能没机会执行
现代 Java 里,关闭文件、流、连接这类资源,更推荐 try-with-resources:
try (java.io.BufferedReader reader = java.nio.file.Files.newBufferedReader(java.nio.file.Path.of("a.txt"))) { System.out.println(reader.readLine());} catch (java.io.IOException e) { System.out.println("read fail: " + e.getMessage());}更完整的异常处理见 Java 异常处理。
finalize:GC 回收前的钩子(已废弃)
finalize 是 Object 定义的 protected 方法:
protected void finalize() throws Throwable理论上,对象失去引用、即将被垃圾回收前,JVM 可能调用一次 finalize(),让开发者做最后清理。
class TempFileHolder { private final String path;
TempFileHolder(String path) { this.path = path; }
@Override @Deprecated(since = "9", forRemoval = true) protected void finalize() { System.out.println("maybe cleanup: " + path); }}但它在实际开发里问题很多:
- 调用时机不确定:对象什么时候被回收,JVM 说了算,
finalize()可能很晚才执行,也可能根本不执行 - 性能差:会拖慢 GC,历史上还可能导致对象“复活”
- 不可靠:不能把它当成资源释放机制
- 已被废弃:Java 9 起标记
@Deprecated,后续版本继续推进移除
所以现代 Java 的正确做法是:
- 文件、连接、锁:用
try-with-resources或显式close() - 需要更灵活的清理:Java 9+ 可以用
java.lang.ref.Cleaner - 业务对象生命周期:由应用代码自己管理,不要等 GC
Object 其他常用方法见 Java Object 类。
最小可运行示例:三者同框
public class FinalDemo { public static void main(String[] args) { System.out.println(readWithFinally()); }
static int readWithFinally() { try { return 1; } finally { System.out.println("finally runs before return"); } }}
final class Demo { final int value = 42;}运行 FinalDemo 会先打印 finally runs before return,再结束方法。
这段代码里:
final class Demo和final int value体现finalfinally块体现收尾执行时机- 没有演示
finalize,因为正常项目里不应再依赖它
常见陷阱
1. 把 finalize 当成析构函数
C++ 的析构函数会在对象离开作用域时确定执行;Java 的 finalize() 没有这种保证。资源释放必须在业务代码里完成,不能等 GC。
2. 以为 final 修饰对象后,对象内容也不能变
final 只锁引用,不锁对象内部状态。集合、数组、自定义对象字段仍可能继续变化。
3. 在 finally 里写业务分支
finally 适合关资源、打日志,不适合塞复杂业务判断。否则 try、catch、finally 三条路径交织,代码很难读。
4. 面试只背名字,不背“属于哪一层”
正确答法要先分类型:
- 关键字 →
final - 语法块 →
finally - 方法 →
finalize
面试高频问题
1. final、finally、finalize 有什么区别?
final 是关键字,限制变量、方法、类不能再被修改;finally 是异常处理语法块,保证收尾逻辑通常都会执行;finalize 是 Object 方法,和 GC 回收有关,但已废弃,不应再用于资源释放。
2. finally 一定会执行吗?
大多数正常路径下会执行,包括 try 或 catch 里遇到 return 的情况。极端情况下,比如 System.exit()、JVM 崩溃,finally 可能不执行。
3. 为什么不再推荐使用 finalize?
因为它执行时机不可控、性能开销大、容易误导开发者把资源释放交给 GC。现代 Java 更推荐 try-with-resources、AutoCloseable 和 Cleaner。
4. final 修饰的基本类型和引用类型有什么区别?
基本类型:final 后值不能改。引用类型:final 后引用不能指向新对象,但对象本身的状态仍可能改变。
一句话总结
final 定规则、finally 做收尾、finalize 曾是 GC 钩子但已过时;面试和写代码时,先分清“关键字、语法块、方法”这三层,再谈具体场景。