finally 真的是「一定會執行」嗎?本文透過 4 個反直覺案例、官方規範原文、JVM 子程式插入機制、返回值保留槽原理,徹底揭開 finally 的真實面貌。
public static int test() {
int i = 1;
// return 0; // 若取消註解
i = i / 0; // 拋 ArithmeticException
try { return i; } finally { System.out.println("finally"); }
}
結果:finally 不執行。原因:try 區塊根本未進入,對應的 finally 自然不執行。
try {
System.out.println("try");
System.exit(0); // 終止 JVM
return 1;
} finally {
System.out.println("finally"); // 不執行
}
結果:JVM 直接終止,finally 無機會運行。
執行 try/catch 期間,執行緒 interrupt() 或 stop()(已棄用),finally 可能不執行。
極端物理環境,JVM 無法保證任何代碼執行。
The Java Tutorials:
The finally block always executes when the try block exits. This ensures that the finally block is executed even if an unexpected exception occurs. But finally is useful for more than just exception handling — it allows the programmer to avoid having cleanup code accidentally bypassed by a return, continue, or break.Note:If the JVM exits while the try or catch code is being executed, then the finally block may not execute. Likewise, if the thread executing the try or catch code is interrupted or killed, the finally block may not execute even though the application as a whole continues.
JLS 12.4:
If a finally clause is present with a try, its code is executed after all other processing in the try is complete. This happens no matter how completion was achieved, whether normally, through an exception, or through a control flow statement such as return or break.
try { System.out.println("try"); return; }
finally { System.out.println("finally"); }
輸出:try → finally。finally 在 return 前執行。
try { System.out.println("try"); int i = 1/0; return 1; }
catch (Exception e) { System.out.println("catch"); return 2; }
finally { System.out.println("finally"); }
輸出:try → catch → finally。finally 在 catch 的 return 前執行。
public static int getValue() {
try { return 0; }
finally { return 1; } // 覆蓋 try 的返回值
}
結果:返回 1。finally 的 return 截獲控制權並返回。
public static int getValue() {
int i = 1;
try { return i; } // 返回值 1 已保存到局部變數表
finally { i++; } // 修改 i 為 2,但返回值槽已鎖定為 1
}
結果:返回 1(非 2)。
finally 代碼編譯為子程式(JVM jsr/ret 指令,現代編譯器多改為內聯複製)。try/catch 的每個控制轉移點(return、throw、break、continue)插入子程式調用。
// 簡化邏輯
try {
int result = compute(); // 計算返回值
// 保存到局部變數表槽位
localVarTable[slot] = result;
// 調用 finally 子程式
jsr finallySubroutine;
// 從槽位恢復
result = localVarTable[slot];
ireturn; // 返回
} finally {
// finally 代碼...
}
關鍵點:保留槽機制僅適用於 return/throw(有返回值/異常對象)。break/continue 無返回值,不觸發保留槽邏輯。
| 場景 | 建議 |
|---|---|
| 資源清理 | 優先用 try-with-resources (JDK 7+),自動關閉,無 finally 陷阱 |
| 必須用 finally | 不要在 finally 中 return/throw/break/continue |
| 修改返回值 | 在 try/catch 完成計算後、finally 前賦值給最終變數,再返回 |
| 異常處理 | catch 區塊處理異常,finally 只做清理,不改變控制流 |