1. 簡介
Java 標準程式庫已經提供了許多例外:NullPointerException、IllegalArgumentException、IOException 等等。但有時標準例外不足以清楚地描述你的程式中發生的錯誤。
實際範例:
你正在撰寫銀行應用。使用者嘗試提領超過帳戶餘額的金額。你可以拋出 IllegalArgumentException。但若改用自訂例外,例如 InsufficientFundsException,語意會清楚得多,讀到程式碼就能立刻明白發生了什麼事。
自訂例外是一種恰到好處的應用程式客製化。它讓你能更細緻地處理問題,並且僅從名稱(前提是命名合理!)就能看出發生了什麼。此外,它們具有自我文件化的特性:方法宣告中的 throws MyException 會直接告訴讀者可能出現哪些錯誤。再者,你也能加入額外欄位(例如餘額、交易金額等)。
2. 如何建立自己的例外?
很簡單:建立一個新類別,繼承自某個標準例外類別。
- 對於 受檢(checked)例外 — 繼承自 Exception。
- 對於 未受檢(unchecked)— 繼承自 RuntimeException。
範例:受檢例外
public class InvalidCredentialsException extends Exception {
public InvalidCredentialsException(String message) {
super(message); // 將訊息傳給父類別
}
}
現在你可以在程式碼中拋出這個例外:
if (!login.equals("admin") || !password.equals("1234")) {
throw new InvalidCredentialsException("帳號或密碼錯誤");
}
範例:未受檢例外
public class NegativeBalanceException extends RuntimeException {
public NegativeBalanceException(String message) {
super(message);
}
}
何時使用受檢,何時使用未受檢?
- 受檢 — 當錯誤是可預期且可處理的(例如驗證錯誤、檔案不存在、使用者提供的資料不正確)。
- 未受檢 — 當錯誤與程式邏輯中的 bug 有關(例如除以零、破壞不變式)。
3. 建構子:如何讓例外資訊更完整
通常,例外類別至少會實作一個帶有參數 String message 的建構子。但也常會加入其他版本:
public class ScoreLimitExceededException extends Exception {
public ScoreLimitExceededException() {
super();
}
public ScoreLimitExceededException(String message) {
super(message);
}
public ScoreLimitExceededException(String message, Throwable cause) {
super(message, cause);
}
public ScoreLimitExceededException(Throwable cause) {
super(cause);
}
}
說明:
- message — 錯誤的文字描述。
- cause — 原因(另一個例外),當你想要「包裹」一個錯誤於另一個錯誤時使用。
建議: 如果不確定需要哪些建構子,至少提供接收字串的那一個。
4. 在程式碼中使用自訂例外
來看個例子:我們有一個使用者,該使用者有分數,且不得將分數加到超過 100。
public class User {
private String name;
private int score;
public User(String name) {
this.name = name;
this.score = 0;
}
public void addScore(int points) throws ScoreLimitExceededException {
if (score + points > 100) {
throw new ScoreLimitExceededException("超出分數上限!嘗試加入: " + points);
}
this.score += points;
}
}
例外類別:
public class ScoreLimitExceededException extends Exception {
public ScoreLimitExceededException(String message) {
super(message);
}
}
處理:
try {
user.addScore(60);
user.addScore(50); // 這裡會拋出例外!
} catch (ScoreLimitExceededException e) {
System.out.println("錯誤: " + e.getMessage());
}
結果:
錯誤: 超出分數上限!嘗試加入: 50
你可能會問:如果不用例外,而是用條件 if,例如回傳 false 或其他特殊值來表示操作失敗,行不行?例如:
public boolean addScore(int points) {
if (score + points > 100) {
return false; // 或者乾脆丟個 RuntimeException,如果你不想處理
}
this.score += points;
return true;
}
雖然這看起來更簡單,但在面對嚴重錯誤或應用程式邏輯被破壞時,這種做法有缺點。
首先,回傳 false 或其他值來表示錯誤,要求呼叫端 一律檢查 傳回值。若開發者忘了這麼做,錯誤可能被忽略,導致不可預期的行為。相反地,例外會 強制處理(對受檢例外而言),或者至少在未攔截時 清楚地發出問題訊號。
其次,例外 更能清楚地傳達錯誤語意。回傳 false 可能代表任何事情:「失敗」、「不適用」、「不可用」。例外 ScoreLimitExceededException 則明白指出:「分數上限已被超過」。這能提升程式的可讀性與可維護性。
第三,例外允許你 集中處理錯誤。與其在所有呼叫 addScore 的地方散落 if 檢查,你可以在單一位置攔截例外並採取相應措施:提示使用者、寫入日誌或回滾交易。
最後,像超出上限這類問題,確實是 例外情況(名稱也由此而來)。正常的執行流程假設分數會成功加入;若不是如此,代表商業邏輯或物件不變式遭到破壞,正是使用例外的理想場景。
5. 在例外中加入自訂欄位
有時把額外資料放進自訂例外很有幫助,有助於後續處理。
範例:
public class ScoreLimitExceededException extends Exception {
private int currentScore;
private int attemptedAdd;
public ScoreLimitExceededException(String message, int currentScore, int attemptedAdd) {
super(message);
this.currentScore = currentScore;
this.attemptedAdd = attemptedAdd;
}
public int getCurrentScore() {
return currentScore;
}
public int getAttemptedAdd() {
return attemptedAdd;
}
}
使用:
if (score + points > 100) {
throw new ScoreLimitExceededException(
"超出分數上限!",
this.score,
points
);
}
6. 實用細節
如何命名你的例外?
在 Java 中,慣例是以後綴 Exception 命名自訂例外: InvalidUserInputException、InsufficientFundsException、ScoreLimitExceededException。
不要把你的例外命名成 Error 或 Warning —— 這可能會誤導其他開發者(甚至幾週後也會誤導你自己)。
在哪裡與何時拋出自訂例外?
- 進行使用者資料驗證時(例如名稱為空、年齡為負數)。
- 違反商業規則時(例如超出上限、嘗試提領超過帳戶餘額)。
- 與外部服務互動發生錯誤時(例如服務不可用、逾時)。
7. 建立自訂例外的常見錯誤
錯誤 № 1:繼承自錯誤的類別。
請繼承自 Exception(或 RuntimeException),而不是 Throwable 或 Error。
錯誤 № 2:沒有加入帶訊息的建構子。
沒有接受字串(String message)的建構子,你的例外將缺乏訊息,會很難除錯。
錯誤 № 3:用標準例外處理商業邏輯。
在需要自訂且「能說明問題」的例外時,不要丟出 NullPointerException 或 IllegalArgumentException。
錯誤 № 4:濫用自訂例外。
不要為每個小問題都建立一個例外類別。若錯誤並非你的領域所特有,請使用標準例外。
錯誤 № 5:缺少序列化(少見但可能)。
如果你的例外需要透過網路傳送或被保存,應實作 implements Serializable。不過對簡單的應用來說,這並非關鍵。
GO TO FULL VERSION