異常處理是 Java 程序健壯性的基石。本文系統梳理異常體系結構、核心 API、捕獲與拋出機制、自定義異常設計、`finally` 語義、以及實戰最佳實踐,助你寫出可維護、可診斷的異常處理代碼。
java.lang.Object
└── java.lang.Throwable (所有異常/錯誤的父類)
├── java.lang.Error (嚴重錯誤,程序無法恢復,不應捕獲)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ ├── NoClassDefFoundError
│ └── ...
└── java.lang.Exception (異常,程序可捕獲處理)
├── java.lang.RuntimeException (運行時異常,非受檢)
│ ├── NullPointerException
│ ├── IllegalArgumentException
│ ├── IndexOutOfBoundsException
│ ├── ArithmeticException
│ └── ...
└── 受檢異常 (Checked Exception, 編譯強制處理)
├── IOException
├── SQLException
├── ClassNotFoundException
├── InterruptedException
└── ...
| 維度 | Error | RuntimeException (非受檢) | 受檢異常 (Checked) |
|---|---|---|---|
| 原因 | JVM/硬件/系統級故障 | 邏輯錯誤/編程疏忽 | 外部環境/資源不可控 |
| 可恢復性 | 不可恢復 | 可通過代碼修正避免 | 可預期並編碼處理 |
| 編譯檢查 | 無 | 無 | 強制 try-catch 或 throws |
| 典型處理 | 記錄日誌、優雅退出 | 修復 Bug、參數校驗 | try-catch 處理/向上拋出 |
| 繼承關係 | 繼承 Error | 繼承 RuntimeException | 繼承 Exception (非 RuntimeException) |
Throwable t = new Exception("詳細信息");
// 1. 獲取簡短描述:類全名 + ": " + message
String toString = t.toString(); // "java.lang.Exception: 詳細信息"
// 2. 獲取詳細信息 (構造器傳入的 message)
String msg = t.getMessage(); // "詳細信息"
// 3. 打印堆棧追蹤到標準錯誤流
t.printStackTrace();
// 輸出格式:
// java.lang.Exception: 詳細信息
// at com.example.Main.method(Main.java:10)
// at com.example.Main.main(Main.java:5)
// 4. 獲取堆棧追蹤元素數組 (程序化分析)
StackTraceElement[] elements = t.getStackTrace();
// 5. 獲取原因鏈 (鏈式異常)
Throwable cause = t.getCause();
t.initCause(cause); // 設置原因
try {
// 高風險代碼
int result = 10 / 0;
String s = null; s.length();
} catch (ArithmeticException | NullPointerException e) { // Java 7+ 多捕獲
// 處理特定異常
System.err.println("捕獲到: " + e.getClass().getSimpleName());
e.printStackTrace();
} catch (Exception e) {
// 捕獲父類異常 (放在最後)
System.err.println("其他異常: " + e.getMessage());
} finally {
// 無論是否異常、是否 return/throw,均執行 (除非 System.exit)
System.out.println("finally 執行: 資源清理、鎖釋放、關閉連接");
}
try (InputStream in = new FileInputStream("a.txt");
OutputStream out = new FileOutputStream("b.txt")) {
// 使用資源
} catch (IOException e) {
// 處理異常
} // 編譯器自動生成 finally 關閉邏輯,且異常抑制機制完善
| 關鍵字 | 位置 | 作用 | 後接 |
|---|---|---|---|
| throw | 方法體內部 | 拋出一個異常對象實例 | `new ExceptionType("msg")` |
| throws | 方法聲明上 | 聲明方法可能拋出的異常類型 | 異常類列表,逗號分隔 |
public void readFile(String path) throws IOException, ParseException { // 聲明
if (path == null) {
throw new IllegalArgumentException("路徑不能為空"); // 拋出非受檢,無需聲明
}
try (FileInputStream fis = new FileInputStream(path)) {
// ...
} catch (FileNotFoundException e) {
// 包裝為業務異常拋出
throw new BusinessException("FILE_NOT_FOUND", "文件不存在: " + path, e);
}
}
// 非受檢業務異常 (推薦繼承 RuntimeException,減少 throws 污染)
public class BusinessException extends RuntimeException {
private final int errorCode; // 業務錯誤碼
private final String errorMsg; // 用戶可讀訊息
private final Object[] args; // 訊息佔位符參數
public BusinessException(int errorCode, String errorMsg, Object... args) {
super(errorMsg);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
this.args = args;
}
// Getters...
}
// 受檢異常 (需強制調用者處理的場景,如檢查型業務規則)
public class ValidationException extends Exception {
private final List errors;
public ValidationException(List errors) {
super("參數校驗失敗");
this.errors = errors;
}
}
public int testFinally() {
try {
return 1; // 1. 計算返回值 1,暫存
} finally {
System.out.println("finally"); // 2. 執行 finally
// return 2; // ⚠️ 若這裡 return,會覆蓋 try 的返回值!極度不推薦
}
// 3. 返回暫存值 1
}
// ❌ 手動 finally 關閉 (繁瑣、易漏、異常抑制問題)
InputStream in = null;
try {
in = new FileInputStream("a.txt");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) try { in.close(); } catch (IOException ignored) {}
}
// ✅ try-with-resources (Java 7+)
try (InputStream in = new FileInputStream("a.txt")) {
// 使用
} catch (IOException e) {
// 處理
}
// 編譯器生成:
// 1. 正常/異常退出時調用 close()
// 2. 若 try 塊拋異常且 close() 也拋異常,close() 的異常被「抑制」附加到主異常 (getSuppressed())
會導致異常丟失或返回值覆蓋,極難排查。靜態分析工具 (SpotBugs, SonarQube) 會報警。
Java 異常處理的核心在於清晰的責任劃分:誰拋出、誰捕獲、誰處理、誰記錄。遵循「具體異常具體捕獲、資源用 try-with-resources、業務異常非受檢並帶錯誤碼、統一攔截轉換為友好響應」原則,即可構建健壯、可觀測、可維護的異常處理體系。