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。