異常處理是 Java 程式健壯性的基石。本文聚焦三大核心難點:**子類重寫父類方法的異常聲明規則**、**子類構造器的異常拋出約束**、以及 **finally 塊中導致異常丟失/吞噬的陷阱**,結合《Thinking in Java》經典案例深度剖析。
子類重寫方法拋出的檢查型異常,必須是父類方法聲明異常的子類或相同異常;不能拋出父類未聲明的檢查型異常。 運行時異常與 Error 無此限制。
// 父類
public abstract class Inning {
public void walk() throws BaseException {}
public abstract void sing() throws BaseException;
}
class BaseException extends Exception {}
class SubException extends BaseException {}
class CupException extends Exception {} // 與 BaseException 無關
// 子類
public class SubInning extends Inning {
// ✅ 合法:拋出父類聲明異常
@Override public void walk() throws BaseException {}
// ✅ 合法:拋出父類異常的子類
@Override public void sing() throws SubException {}
// ❌ 編譯錯誤:拋出父類未聲明的檢查型異常
// @Override public void walk() throws CupException {}
// ✅ 合法:不拋出任何異常(子類可以更「安全」)
@Override public void walk() {}
}
public void process(Inning in) {
try {
in.walk(); // 編譯器只知道 Inning.walk() 拋出 BaseException
} catch (BaseException e) { ... }
}
// 若子類 SubInning.walk() 拋出 CupException:
Inning in = new SubInning();
process(in); // 運行時拋出 CupException,但 process 僅捕獲 BaseException → 異常無法被捕獲,破壞多態契約
編譯器強制子類遵守父類的「異常契約」,保證父類引用調用時,異常處理邏輯依然有效。
子類構造器必須聲明拋出父類構造器聲明的所有檢查型異常,或其父類異常。不能拋出父類未聲明的新檢查型異常。
public abstract class Base {
public Base() throws SubException {} // 聲明拋出 SubException
}
class SubException extends Exception {}
class FatherException extends Exception {}
class PenException extends Exception {}
// 子類構造器
public class Derived extends Base {
// ✅ 必須聲明拋出 SubException 或其父類 FatherException/Exception
public Derived() throws FatherException {
// 隱式 super() 拋出 SubException,FatherException 是其父類,編譯通過
}
// ❌ 編譯錯誤:PenException 與 SubException 無關
// public Derived() throws PenException {}
}
子類構造器執行前必須調用父類構造器(顯式或隱式 `super()`),父類構造器拋出的異常必須被子類構造器聲明繼續拋出或捕獲處理,否則異常會「漏」出構造器,違背異常處理強制機制。
public class FinallyLostException {
public static void main(String[] args) {
try {
try {
throw new RedException(); // 異常 A
} finally {
throw new BlueException(); // 異常 B:覆蓋 A!
}
} catch (Exception e) {
System.out.println(e); // 僅輸出 BlueException,RedException 丟失!
}
}
}
class RedException extends Exception {}
class BlueException extends Exception {}
後果:原始異常(`RedException`)被「吞噬」,堆疊追蹤丟失,排查極其困難。
最佳實踐:
public class FinallyReturn {
public static void main(String[] args) {
try {
throw new Exception("原始異常");
} finally {
return; // 直接返回,異常被「吃掉」,程式正常結束,無任何輸出!
}
}
}
嚴重性:異常無聲無息消失,日誌無記錄,監控無告警,極難發現 Bug。
鐵律:**finally 塊中絕對不要寫 `return`、`throw`、`break`、`continue` 等改變控制流的語句。**
| 場景 | 規則 | 核心原則 |
|---|---|---|
| 方法重寫 | 子類異常 ⊆ 父類異常(檢查型) | 維護多態契約,父類引用調用安全 |
| 構造器 | 子類必須聲明父類構造器異常 | 隱式 super() 異常必須傳播或處理 |
| finally 拋異常 | 覆蓋 try/catch 異常 → 丟失 | finally 僅做清理,不拋業務異常 |
| finally return | 吞噬所有異常 → 靜默失敗 | 禁止在 finally 改變控制流 |
| 最佳實踐 | try-with-resources (Java 7+) | 自動資源管理,異常抑制機制安全 |
異常處理不僅是 `try-catch-finally` 的語法,更是**契約設計**與**資源安全**的體系。掌握重寫規則保證繼承體系健壯,理解構造器異常傳播避免初始化洩漏,警惕 finally 陷阱防止異常神秘消失——這是從「會用」到「用好」 Java 異常的關鍵躍升。