貼心還是造成麻煩?


例外處理的本意是,在程式錯誤發生時,能夠有明確的方式通知API客戶端,讓客戶端採取進一步的動作修正錯誤,而就撰寫本文的時間點來說,Java是唯一採用受檢例外(Checked exception)的語言,這有兩個目的:一是文件化,受檢例外宣告會是API操作介面的一部份,客戶端只要查閱文件,就可以知道方法可能會引發哪些例外,並事先加以處理,而這是API設計者決定是否拋出受檢例外時的考量之一,另一個目的是提供編譯器資訊,讓編譯器能夠在編譯時期就檢查出API客戶端沒有處理例外。

問題是有些錯誤發生而引發例外時,你根本無力回復,例如SQLException是受檢例外,如果例外的發生原因是資料庫連線異常,而連線異常的原因是由於實體線路問題,那麼無論如何你都不可能使用trycatch回復到正常可運作的情況。

要抓還是要拋? 中提到,錯誤發生時,如果上下文環境並沒有足夠的資訊讓你處理例外,你可以就現有資訊處理完例外後,重新拋出例外。你也許會這麼寫:

public Customer getCustomer(String id) throws SQLException {
    ...
}

看起來似乎沒有問題,但假設這個方法是在整個應用程式非常底層被呼叫,在某個SQLException發生時,最好的方法是將例外浮現至呈現層,例如網頁技術,將錯誤訊息於網頁上顯示出來給管理人員。

為了讓例外往上浮現,你也許會選擇在每個方法呼叫上都宣告throws SQLException,但前面假設,這個方法的呼叫是在整個應用程式的底層,這樣的作法也許會造成許多程式碼的修改(更別說要重新編譯了),另一個問題是,如果你根本無權修改應用程式的其它部份,這樣的作法顯示行不通。

受檢例外本意良好,有助於程式設計人員注意到例外的可能性並加以處理,但在應用程式規模增大時,會對逐漸對維護造成困難,上述情況不一定是你自訂API時發生,也可能是在底層引入了一個會拋出受檢例外的API而發生類似情況。

重新拋出例外時,除了將捕捉到的例外直接拋出,也可以考慮為應用程式自訂專屬例外類別,讓例外更能表現應用程式特有的錯誤資訊。自訂例外類別時,可以繼承ThrowableErrorException的相關子類別,通常建議繼承自Exception,如果不是繼承自ErrorRuntimeException,那麼就會是受檢例外。

public class CustomizedException extends Exception { // 自訂受檢例外的一個例子
    ...
}

錯誤發生時,如果上下文環境並沒有足夠的資訊讓你處理例外,你可以就現有資訊處理完例外後,重新拋出例外,既然你已經針對錯誤做了某些處理,那麼也就可以考慮自訂例外,用以更精確地表示出未處理的錯誤,如果認為呼叫API的客戶端應當有能力處理未處理的錯誤,那就自訂受檢例外、填入適當錯誤訊息並重新拋出,並在方法上使用throws加以宣告,如果認為呼叫API的客戶端沒有準備好就呼叫了方法,才會造成還有未處理的錯誤,那就自訂非受檢例外、填入適當錯誤訊息並重新拋出。

public class CustomizedException extends RuntimeException { // 自訂非受檢例外的一個例子
    ...
}

一個基本的例子是這樣的:

try {
    ....
} catch(SomeException ex) {
    // 作些可行的處理
    // 也許是 Logging 之類的
    throw new CustomizedException("error message..."); // Checked 或 Unchecked?
}

類似地,如果流程中要拋出例外,也要思考一下,這是客戶端可以處理的例外嗎?還是客戶端沒有準備好前置條件就呼叫方法,才引發的例外?

if(someCondition) {
    throw new CustomizedException("error message"); // Checked 或 Unchecked?
}

無論如何,Java採用了受檢例外的作法,Java的標準API似乎也打算一直這麼區分下去,只是受檢例外讓開發人員無從選擇,會由編譯器強制性要求處理,確實會在設計上造成麻煩,因而有些開發者在設計程式庫時,乾脆就選擇完全使用非受檢例外,一 些會封裝應用程式底層行為的框架,如Spring或Hibernate,就選擇了讓例外體系是非受檢例外,例如Spring中的DataAccessException,或者是Hibernate 3中的HibernateException,它們選擇給予開發人員較大的彈性來面對例外(也許也需要開發人員更多的經驗)。

隨著應用程式的演化,例外也可以考慮演化,也許一開始是設計為受檢例外,然而隨著應用程式堆疊的加深,受檢例外老是一層一層往外宣告拋出造成麻煩時,這也許代表了,原先認為客戶端可處理的例外,每一層客戶端實際上都無力處理了,每層客戶端都無力處理的例外,也許該視為一種臭蟲,也許客戶端在呼叫時都該準備好前置條件再行呼叫,以避免引發錯誤,這時將受檢例外演化為非受檢例外,也許就有其必要。

實際上確實有這類例子,Hibernate 2中的HibernateException是受檢例外,然而Hibernate 3中的HibernateException變成了非受檢例外。

然而,即使不用面對受檢例外與非受檢例外的區別,開發者仍然必須思考,這是客戶端可以處理的例外嗎?還是客戶端沒有準備好前置條件就呼叫方法,才引發的例外?