Java 將異常分為兩大類:受檢異常 與 非受檢異常。這不僅是語法規則差異,更反映了錯誤處理的兩種哲學:前者代表「可預期且可恢復的業務異常」,後者代表「程式邏輯錯誤或系統級故障」。深入理解兩者機制與適用場景,是編寫健壯 Java 程式的關鍵。
classDiagram
Throwable <|-- Exception
Throwable <|-- Error
Exception <|-- RuntimeException
Exception <|-- IOException
Exception <|-- SQLException
Exception <|-- ClassNotFoundException
RuntimeException <|-- NullPointerException
RuntimeException <|-- IllegalArgumentException
RuntimeException <|-- ArrayIndexOutOfBoundsException
Error <|-- OutOfMemoryError
Error <|-- StackOverflowError
// 自訂受檢異常
class BusinessException extends Exception {
public BusinessException(String msg) { super(msg); }
}
public class ExceptionFlowDemo {
// 方法 1:拋出受檢異常 → 必須宣告 throws
public void methodA() throws BusinessException {
if (Math.random() > 0.5) throw new BusinessException("業務規則違反");
}
// 方法 2:拋出非受檢異常 → 無需宣告
public void methodB(String input) {
if (input == null) throw new IllegalArgumentException("參數不可為 null");
}
// 方法 3:呼叫 methodA 但不捕獲 → 必須繼續拋出
public void methodC() throws BusinessException {
methodA(); // 編譯器強制:要么 catch,要么 throws
}
public static void main(String[] args) {
ExceptionFlowDemo demo = new ExceptionFlowDemo();
// 呼叫受檢異常方法 → 必須 try-catch
try {
demo.methodA();
demo.methodC();
} catch (BusinessException e) {
System.out.println("捕獲業務異常: " + e.getMessage());
}
// 呼叫非受檢異常方法 → 可選擇性捕獲
demo.methodB(null); // 拋出 IllegalArgumentException,若不捕獲將向上傳播至 JVM
}
}
核心特徵:呼叫方能夠且應當處理的情況,屬於正常業務流程的一部分。
public class UrlFetcher {
public String fetchContent(String urlString) {
try {
URL url = new URL(urlString); // 拋出 MalformedURLException (Checked)
// ... 讀取內容
return "內容";
} catch (MalformedURLException e) {
// 呼叫方可處理:提示使用者重新輸入
return "錯誤:URL 格式無效,請檢查輸入";
} catch (IOException e) {
// 網路異常:可重試或記錄日誌
return "錯誤:網路連線失敗";
}
}
}
核心特徵:呼叫方無法在執行期合理恢復的情況,通常源於程式邏輯缺陷或環境極限。
// ❌ 錯誤做法:掩蓋 Bug
public int calculateAverage(int[] numbers) {
try {
int sum = 0;
for (int i = 0; i <= numbers.length; i++) { // Bug: <= 應為 <
sum += numbers[i];
}
return sum / numbers.length;
} catch (ArrayIndexOutOfBoundsException e) {
log.error("計算失敗", e); // 記錄日誌但程式繼續跑,結果錯誤難發現
return 0;
}
}
// ✅ 正確做法:修正邏輯,讓異常暴露
public int calculateAverage(int[] numbers) {
if (numbers == null || numbers.length == 0) {
throw new IllegalArgumentException("陣列不可為空");
}
int sum = 0;
for (int num : numbers) { // 使用增強 for 迴圈避免索引錯誤
sum += num;
}
return sum / numbers.length;
}
graph TD
A[發生錯誤情況] --> B{呼叫方能否合理處理並恢復?}
B -->|是| C[Checked Exception]
B -->|否| D{是否為程式邏輯錯誤?}
D -->|是| E[Unchecked Exception - RuntimeException]
D -->|否| F{是否為系統級災難性故障?}
F -->|是| G[Error]
F -->|否| H[重新評估設計]
| 原則 | 說明 | 範例 |
|---|---|---|
| API 契約明確化 | Checked Exception 為方法簽名一部分,文件化預期失敗模式 | `FileInputStream` 拋出 `FileNotFoundException` |
| 避免濫用 Checked | 若呼叫方幾乎總是 `catch { throw new RuntimeException(e); }`,應改為 Unchecked | JDBC `SQLException` 常被包裝為 `DataAccessException` |
| 保留堆疊追蹤 | 包裝異常時使用 `throw new CustomException(cause)` 而非 `throw new CustomException(e.getMessage())` | Spring `DataAccessException` 保留 SQL 錯誤碼 |
| 文件化 Unchecked 前置條件 | Javadoc `@param` `@throws` 說明何時拋出 `IllegalArgumentException` | `List.get(int index)` 文件說明索引越界拋出例外 |
| 錯誤碼 vs 異常 | 高頻、可預期失敗(如驗證)可考慮 `Result<T, E>` 模式替代異常 | Vavr `Either`、Java 17+ `sealed interface Result` |
public class ServiceException extends RuntimeException {
private final String errorCode;
public ServiceException(String errorCode, String message, Throwable cause) {
super(message, cause); // 關鍵:保留原始原因
this.errorCode = errorCode;
}
public String getErrorCode() { return errorCode; }
}
// 使用範例
public User findUserById(Long id) {
try {
return jdbcTemplate.queryForObject("SELECT * FROM users WHERE id=?",
new UserRowMapper(), id);
} catch (EmptyResultDataAccessException e) {
throw new ServiceException("USER_NOT_FOUND", "使用者不存在: " + id, e);
} catch (DataAccessException e) {
throw new ServiceException("DB_ERROR", "資料庫存取失敗", e);
}
}
// Java 17+ 型別模式匹配
try {
riskyOperation();
} catch (IOException | SQLException e) { // 多重捕獲
log.error("資源存取失敗", e);
throw new ServiceException("IO_ERROR", "儲存層異常", e);
} catch (RuntimeException e) { // 最後兜底
log.error("未預期邏輯錯誤", e);
throw e; // 重新拋出,讓全域異常處理器接管
}
Java 異常機制的雙軌制設計,本質上是將「業務流程的可預期分支」與「程式缺陷的不可預期終止」分離。優秀的異常設計應做到:
記住:異常是給開發者看的,錯誤訊息是給使用者看的。兩者分離處理,才能兼顧系統穩健與使用者體驗。