try-catch-finally 結合 return 是 Java 面試高頻考點,也是生產 Bug 溫床。多數開發者只記結論「finally 會執行、finally 有 return 會覆蓋」,卻不知為何覆蓋、異常路徑怎麼走、JVM 怎麼保證語義。本文基於 JVM 規格 §3.13 Compiling finally,結合 javap -c 實測字節碼,徹底講透控制流交互細節。
// 源碼
int m() {
try { return 1; }
finally { System.out.println("finally"); }
}
// 關鍵字節碼片段
iconst_1
istore_1 // 【暫存返回值到局部變數 1】
getstatic System.out
ldc "finally"
invokevirtual println
iload_1 // 恢復返回值
ireturn
結論:返回值在進入 finally 前暫存,finally 結束後恢復返回,**finally 對變數修改不影響返回值**。
int m() {
try { throw new Exception(); }
catch (Exception e) { return 2; }
finally { System.out.println("finally"); }
}
// 字節碼流程
// 1. 異常表捕獲 → 目標 catch 區塊
// 2. catch 區塊:iconst_2 → istore_1 (暫存)
// 3. 執行 finally (同樣內聯)
// 4. iload_1 → ireturn
結論:catch 返回值同樣暫存機制,finally 不影響。
int m() {
try { return 1; }
finally { return 2; }
}
// 字節碼關鍵差異
iconst_1
istore_1 // 暫存 1
iconst_2
ireturn // 【直接返回 2,不再恢復 istore_1】
結論:finally 中的 return 產生新的控制流理由,**直接覆蓋**舊理由,舊返回值被丟棄。
int m() {
try { return 1; }
finally { throw new RuntimeException(); }
}
// 字節碼
iconst_1
istore_1
new RuntimeException
dup
invokespecial init
athrow // 直接拋出,返回值 1 徹底丟失
Exception table:
from to target type
0 3 6 Class java/lang/Exception
6 7 6 Class java/lang/Exception // finally 內異常自我保護
from-to:受保護字節碼範圍(try 區塊)target:異常處理入口(catch 或 finally 子程式入口)type:捕獲異常類型,any 表示所有JVM 在異常表中為 finally 區塊自身也註冊一條 target=自身入口 的記錄。若 finally 內部拋異常,**外層異常傳播中斷**,改拋 finally 的異常(符合 JLS「新理由覆蓋舊理由」)。
jsr (Jump SubRoutine) / ret 實現 finally 代碼複用,但難以優化、驗譐複雜。List get() {
List list = new ArrayList<>();
try { return list; }
finally { list.add("leaked"); } // 呼叫端拿到已被修改的 list!
}
修正:return Collections.unmodifiableList(new ArrayList<>(list)); 或 return List.copyOf(list); (Java 10+)。
try (AutoCloseable a = () -> { throw new Exception("A"); };
AutoCloseable b = () -> { throw new Exception("B"); }) {
throw new Exception("Main");
}
// 實際拋出:Main
// 抑制順序:B.addSuppressed(A) → Main.addSuppressed(B)
// 即:最後關閉者優先被抑制,try 異常為主異常
public Result doWork(Input in) throws BizException {
Resource res = null;
try {
res = acquire();
return process(res, in); // 正常返回
} catch (IOException | SQLException e) {
throw new BizException("處理失敗", e); // 統一業務異常
} finally {
if (res != null) {
try { res.close(); } // 絕不拋出
catch (Exception ignored) { log.warn("關閉失敗", ignored); }
}
}
}
| 場景 | finally 執行? | 最終結果 | 關鍵機制 |
|---|---|---|---|
| try 正常 return | 是 | try 返回值 | 暫存→執行 finally→恢復 |
| try return + finally return | 是 | finally 返回值 | 新理由覆蓋舊理由 |
| try 拋異常 + catch return | 是 | catch 返回值 | catch 暫存→finally→恢復 |
| catch return + finally return | 是 | finally 返回值 | 覆蓋 |
| finally 拋異常 | 是 | finally 異常 | 覆蓋 try/catch 返回或異常 |
| try System.exit | 否 | JVM 退出 | 進程終止 |
| 線程被 kill | 否 | 線程終止 | OS 強制 |
close()、unlock()、release(),**嚴禁 return/throw/break/continue**。掌握字節碼層面的暫存、覆蓋、異常表機制,你就能在面試、Code Review、線上排查中,對任何 try-catch-finally-return 組合做出**精準預測**——不再靠背誦,而是靠理解。