finally 中代码一定会被执行吗?

#Java#面试八股

finally 中代码一定会被执行吗?

结论:不保证。finally 不是 JVM 的独立指令,而是编译器把同一段代码复制到正常路径、异常路径与 return 路径上(用异常表 exception table 串起来),所以"几乎所有"控制流都会经过它;但进程直接退出、JVM 崩溃、线程非正常终止时不会执行。另外,finally 里的 return 会覆盖 try 的返回值并吞掉异常,这是更常见的"逻辑上执行了、结果却不对"。

1. 编译器怎么实现 finally

源码:                     编译后(概念上):
try {                       try {
  work();                     work();
} finally {                 } catch (任何异常 e) {
  cleanup();                  cleanup();
}                             throw e;
                            }
                            cleanup();      ← 正常路径再复制一份

return 路径:return 前插入 cleanup(),并保存返回值到局部变量槽

正因为是"复制代码",finally 里的 return/throw 会直接改变控制流:return 会覆盖已保存的返回值,throw 会覆盖正在传播的异常。

2. 哪些情况不执行

场景是否执行原因
try 正常结束是正常路径
try 抛异常,catch 捕获后继续是异常路径
try 抛异常,无人捕获是异常表指向 finally,执行后再抛
try 中 return / break / continue是返回前插入 finally
调用 System.exit(0)否直接终止 JVM,不再执行任何 Java 代码
调用 Runtime.getRuntime().halt(0)否比 exit 更粗暴,连 shutdown hook 都不跑
JVM 崩溃(kill -9、段错误、OOM 后被杀)否进程消失
守护线程(daemon thread)随 JVM 退出否主线程结束后守护线程被强制终止
线程被 Thread.stop() 杀死(已废弃)否异步异常,栈被直接展开
try 之前的代码抛异常否还没进入 try
finally 内部自己抛异常已进入,但主异常被覆盖后抛的异常替换了正在传播的异常

3. 代码:两个经典陷阱

public class FinallyDemo {
    /** finally 中 return 覆盖返回值 */
    static int f() {
        try {
            return 1;
        } finally {
            return 2;          // 最终返回 2,try 的 return 被丢弃
        }
    }

    /** finally 修改局部变量,不影响已经"算好"的返回值 */
    static int g() {
        int x = 1;
        try {
            return x;          // 返回值先写入局部变量槽
        } finally {
            x = 2;             // 改的是 x 本身,不影响返回值
        }
    }

    /** finally 吞掉异常 */
    static void h() {
        try {
            throw new IllegalStateException("boom");
        } finally {
            return;            // 异常被静默丢弃,调用方什么都收不到
        }
    }

    public static void main(String[] args) {
        System.out.println(f());   // 2
        System.out.println(g());   // 1
        System.out.println("done");
    }
}

h() 是最危险的一种:方法签名上没有任何异常,调用方也感知不到失败,问题被彻底隐藏。IDE 与静态检查工具通常会对 finally 里的 return 报警,不要忽略。

4. 正确姿势

// 推荐:try-with-resources 自动关闭,异常以 suppressed 形式保留
try (InputStream in = Files.newInputStream(path);
     BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8))) {
    return reader.readLine();
}

// 如果必须手写 finally,不要在里面 return / throw
finally {
    try {
        conn.close();
    } catch (SQLException e) {
        log.warn("close failed", e);    // 记录但不抛出,避免覆盖主异常
    }
}

try-with-resources 的实现细节:编译器生成 close() 调用,若 try 块已抛异常,close() 的异常通过 addSuppressed 附加,主异常仍是业务异常——这正好解决了手写 finally 的覆盖问题。

5. 常见追问

追问回答要点
finally 和 catch 谁先执行?先匹配 catch,catch 执行完(或抛出)后进入 finally
多个 catch 怎么写?子类异常在前、父类在后;JDK 7+ 可用 catch (A | B e) 合并
System.exit 时 shutdown hook 会执行吗?会(exit 触发钩子),halt 不会;但 finally 两者都不会
finally 里能抛异常吗?能,但会覆盖主异常,仅在"清理失败必须上报"时考虑,且要 addSuppressed
finally 中修改返回对象的内容会生效吗?会,引用没变、改的是同一对象;但重新赋值引用不生效
finally 能保证资源一定释放吗?不能保证"一定",只能保证"绝大多数路径";超时/进程被杀仍需外部兜底

小结

finally 由编译器复制代码 + 异常表实现,覆盖正常、异常、返回三类路径,但挡不住进程级退出。写代码记住两条:资源关闭用 try-with-resources,finally 里不写 return/throw。

笔记加载中…