面试官问:final、finally、finalize有什么区别?(附图解+避坑指南)
面试官问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废弃。 关键对比表维度finalfinallyfinalize()类型关键字/修饰符关键字Object类的方法作用定义“不可变性”异常处理后必须执行的代码块对象被GC回收前的回调钩子修饰类类不能被继承——修饰方法方法不能被重写——修饰变量变量值/引用不可变——执行保证编译期保证运行时保证除System.exit(0)外不保证执行版本状态✅ 正常使用✅ 正常使用❌ JDK 9 废弃 → JDK 17 弱化 →JDK 21 彻底移除 面试官追问重点追问1finally 一定执行吗有什么特殊情况回答要点大部分情况下会执行但有两个“死穴”。详细回答finally 块在大多数情况下一定会执行但有两种特殊情况不会System.exit(0)主动终止 JVM 进程finally 不会执行。JVM 崩溃或线程被杀死如物理断电、kill -9强制杀进程等。另外如果在 finally 块中抛出异常或执行return也会影响最终行为。不要在 finally 中写return否则会覆盖 try 中的返回值导致难以排查的 Bug。追问2如果在 try 块中写了 returnfinally 还会执行吗答会执行。finally 块中的代码会在return语句执行之后、方法真正返回给调用者之前执行。这也是为什么不要在 finally 中写return——它会覆盖 try 的返回值。追问3finalize() 为什么被废弃替代方案是什么⭐ 核心加分题回答要点性能差 时机不确定 安全漏洞 有更好的替代方案。详细回答finalize() 被废弃的主要原因执行时机不确定GC 何时调用 finalize() 完全由 JVM 决定不能依赖它来释放关键资源。性能差GC 调用 finalize() 会拖慢垃圾回收效率延长对象生命周期。可能造成对象“复活”在 finalize() 中重新将this赋值给外部引用对象就“复活”了导致 GC 白干。安全隐患恶意代码可以通过重写 finalize() 来复活包含敏感信息如密码的对象导致敏感数据一直驻留在内存中造成信息泄露。JDK 版本演进JDK 9标记Deprecated废弃JDK 17弱化支持推荐使用替代方案JDK 21彻底移除实际在 JDK 18 已标记forRemovaltrue替代方案try-with-resources推荐实现AutoCloseable接口自动释放资源。关闭顺序为LIFO后进先出与构造顺序相反。显式调用close()手动释放资源最常见方式。java.lang.ref.CleanerJDK 9基于PhantomReference虚引用实现比 finalize 更轻量且不阻塞 GC。 Cleaner 实战NIO 直接内存清理Cleaner最典型的应用场景是 NIO 的直接内存Direct Memory。直接内存不受 JVM GC 管控必须显式释放这恰好是Cleaner的核心应用场景。 源码关联JDK 底层ByteBuffer.allocateDirect()创建的直接缓冲区内部封装Cleaner实现堆外内存自动释放这是Cleaner最核心、最广泛的落地场景。Cleanervsfinalize核心区别finalize依赖 JVM 的 GC 周期执行时机不可控Cleaner通过PhantomReference将清理动作与引用队列关联可以在对象变得不可达时及时且异步地执行清理不阻塞 GC 线程性能更优且不会被“对象复活”问题干扰。importjava.lang.ref.Cleaner;// 模拟一个需要被清理的 Native 资源classNativeResourceimplementsRunnable{privatefinalStringname;publicNativeResource(Stringname){this.namename;System.out.println(name 已分配 Native 内存);}Overridepublicvoidrun(){// 实际场景释放 malloc() 分配的内存System.out.println(name 已释放 Native 内存 (Cleaner 触发));}}publicclassCleanerDemo{privatestaticfinalCleanerCLEANERCleaner.create();publicstaticvoidmain(String[]args)throwsInterruptedException{NativeResourceresourcenewNativeResource(DirectBuffer);// 注册清理动作当 resource 对象不可达时自动执行 resource.run()CLEANER.register(resource,resource);resourcenull;// 解除强引用System.gc();// 建议 JVM 执行 GC不保证立即执行Thread.sleep(500);// 若 GC 未及时触发可适当延长休眠时间System.out.println(主程序结束);}}⚠️ Cleaner 生产规范Cleaner清理动作是单线程执行的大量资源清理场景可能造成清理线程阻塞。业务层主动close()始终是首选Cleaner仅作为防御性兜底不可替代显式资源释放。 常见坑点坑1final 修饰引用类型 ≠ 对象不可变finalListStringlistnewArrayList();list.add(hello);// ✅ 合法list 的引用没变但对象内容变了listnewArrayList();// ❌ 编译错误引用不能重新赋值关键final 修饰引用类型时锁住的是引用地址不是对象内容。坑2finally 中写 return 覆盖 try 的返回值publicinttest(){try{return1;}finally{return2;// --- 致命陷阱这会覆盖 try 中的 return 1}}结果返回 2而不是 1。这是生产环境难以排查的 Bug 源头。坑3finalize 中抛出异常导致对象无法回收Overrideprotectedvoidfinalize()throwsThrowable{thrownewRuntimeException();// --- 异常被忽略对象无法及时回收}正确做法永远不要依赖 finalize也不要在其中抛出未捕获的异常。坑3finalize 对象复活演示理解原理// finalize 对象复活演示JDK9已废弃仅用于理解原理staticclassResurrectable{staticResurrectablesaved;Overrideprotectedvoidfinalize()throwsThrowable{System.out.println(finalize 执行对象复活);savedthis;// 重新赋值给静态引用对象“复活”super.finalize();}}// 使用ResurrectableobjnewResurrectable();objnull;System.gc();// 第一次 GC → 对象复活Thread.sleep(100);Resurrectable.savednull;System.gc();// 第二次 GC → 才真正回收关键对象复活后GC 需要第二次才能将其真正回收这是 finalize 导致资源延迟释放的典型例子。坑4try-finally 关闭多个资源时的异常掩盖// ❌ 旧方式第一个 close 的异常会掩盖第二个FileInputStreamfisnull;try{fisnewFileInputStream(test.txt);}finally{if(fis!null)fis.close();// 如果 close 抛异常try 中的异常被吞掉}// ✅ 新方式try-with-resources 自动处理try(FileInputStreamfisnewFileInputStream(test.txt)){// 使用 fis}// 多个资源按反序关闭异常被自动 Suppressed底层本质try-with-resources是 Java 7 引入的语法糖编译后仍然生成try-finally结构多个资源按LIFO顺序关闭。异常抑制通过Throwable.addSuppressed()实现因此主异常不会丢失。 可运行验证代码publicclassFinalFinallyFinalizeDemo{// final 演示 staticclassParent{finalvoidcannotOverride(){System.out.println(父类的 final 方法);}}// class Child extends Parent {// void cannotOverride() { } // ❌ 编译错误不能重写 final 方法// }// finally 演示 publicstaticinttestFinally(){try{System.out.println(try 执行);return1;}finally{System.out.println(finally 执行);// return 2; // --- 如果取消注释返回值变成 2}}publicstaticvoidtestFinallyExit(){try{System.out.println(try 执行);System.exit(0);// JVM 退出}finally{System.out.println(finally 执行);// ❌ 不会执行}}// finalize 演示已废弃仅用于演示 staticclassResource{OverrideDeprecatedprotectedvoidfinalize()throwsThrowable{System.out.println(finalize() 被调用不推荐使用);super.finalize();}}publicstaticvoidmain(String[]args){// 1. finally 与 return 的执行顺序System.out.println(testFinally() 返回值: testFinally());// 2. finalize 演示ResourcernewResource();rnull;System.gc();// 建议 JVM 执行 GC不保证立即执行System.out.println(main 结束);}}输出示例try 执行 finally 执行 testFinally() 返回值: 1 main 结束❓ 评论区挑战问题下面代码的输出是什么为什么publicclassFinallyTest{publicstaticinttest(){try{return10;}finally{return20;}}publicstaticvoidmain(String[]args){System.out.println(test());}}A. 10B. 20C. 编译报错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篇完整学习地图每篇标配生活类比 可运行代码 避坑表 面试高频题 练习题不背八股文真正讲透“为什么”。 《Java 100天进阶之路》完整目录导航学习建议图解系列负责“快速建立知识图谱”进阶系列负责“深入理解原理”两个系列搭配使用面试备考效率翻倍。 ① 你在实际开发中遇到过 finally 中写 return 导致的诡异 Bug 吗 ② 你项目中使用过Cleaner或ByteBuffer.allocateDirect管理直接内存吗欢迎评论区分享你的故事。