Java 異常進階

在使用Java編寫應用的時候,我們常常需要通過第三方類庫來幫助我們完成所需要的功能。有時候這些類庫所提供的很多API都通過throws聲明瞭它們所可能拋出的異常。但是在查看這些API的文檔時,我們卻沒有辦法找到有關這些異常的詳盡解釋。在這種情況下,我們不能簡單地忽略這些由throws所聲明的異常:

public void shouldNotThrowCheckedException {

// 該API調用可能拋出一個不明原因的Checked Exception

exceptionalAPI;

}

否則Java編譯器會由於shouldNotThrowCheckedException函數沒有聲明其可能拋出的Checked Exception而報錯。但是如果通過throws標明瞭該函數所可能拋出的Checked Exception,那麼其它對shouldNotThrowCheckedException函數的調用同樣需要通過throws標明其可能拋出該Checked Exception。

那我們應該如何對這些Checked Exception進行處理呢?在本文中,我們將對如何在Java應用中使用及處理Checked Exception進行簡單地介紹。

Java異常簡介

在詳細介紹Checked Exception所導致的問題之前,我們先用一小段篇幅簡單介紹一下Java中的異常。

在Java中,異常主要分爲三種:Exception,RuntimeException以及Error。這三類異常都是Throwable的子類。直接從Exception派生的各個異常類型就是我們剛剛提到的Checked Exception。它的一個比較特殊的地方就是強制調用方對該異常進行處理。就以我們常見的用於讀取一個文件內容的FileReader類爲例。在該類的構造函數聲明中聲明瞭其可能會拋出FileNotFoundException:

public FileReader(String fileName) throws FileNotFoundException {

……

}

那麼在調用該構造函數的函數中,我們需要通過try…catch…來處理該異常:

public void processFile {

try {

FileReader fileReader = new FileReader(inFile);

} catch(FileNotFoundException exception) {

// 異常處理邏輯

}

……

}

小編是一個有着5年工作經驗的java程序員,對於java,自己有做資料的整合,一個完整學習java的路線,學習資料和工具,相信這裏有很多學習java的小夥伴,我創立了一個2000人學習扣羣,479121291。每晚都有java的直播課程。無論是初級還是進階的小夥伴小編我都歡迎!

如果我們不通過try…catch…來處理該異常,那麼我們就不得不在函數聲明中通過throws標明該函數會拋出FileNotFoundException:

public void processFile throws FileNotFoundException {

FileReader fileReader = new FileReader(inFile); // 可能拋出FileNotFoundException

……

}

而RuntimeException類的各個派生類則沒有這種強制調用方對異常進行處理的需求。爲什麼這兩種異常會有如此大的區別呢?因爲RuntimeException所表示的是軟件開發人員沒有正確地編寫代碼所導致的問題,如數組訪問越界等。而派生自Exception類的各個異常所表示的並不是代碼本身的不足所導致的非正常狀態,而是一系列應用本身也無法控制的情況。例如一個應用在嘗試打開一個文件並寫入的時候,該文件已經被另外一個應用打開從而無法寫入。對於這些情況,Java通過Checked Exception來強制軟件開發人員在編寫代碼的時候就考慮對這些無法避免的情況的處理,從而提高代碼質量。

而Error則是一系列很難通過程序解決的問題。這些問題基本上是無法恢復的,例如內存空間不足等。在這種情況下,我們基本無法使得程序重新回到正常軌道上。因此一般情況下,我們不會對從Error類派生的各個異常進行處理。而且由於其實際上與本文無關,因此我們不再對其進行詳細講解。

天使變惡魔

既然Java中的Checked Exception能夠提高用戶代碼質量,爲什麼還有那麼多人反對它呢?原因很簡單:它太容易被誤用了。而在本節中,我們就將列出這些誤用情況並提出相應的網絡上最爲推薦的解決方案。

無處不在的throws

第一種誤用的情況就是Checked Exception的廣泛傳播。在前面已經提到過,調用一個可能拋出Checked Exception的API時,軟件開發人員可以有兩種選擇。其中一種選擇就是在對該API進行調用的函數上添加throws聲明,並將該Checked Exception向上傳遞:

public void processFile throws FileNotFoundException {

FileReader fileReader = new FileReader(inFile); // 可能拋出FileNotFoundException

……

}

而在調用processFile函數的代碼中,軟件開發人員可能覺得這裏還不是處理異常FileNotFoundException的合適地點,因此他通過throws將該異常再次向上傳遞。但是在一個函數上添加throws意味着其它對該函數進行調用的代碼同樣需要處理該throws聲明。在一個代碼複用性比較好的系統中,這些throws會非常快速地蔓延開來。如果不去處理Checked Exception,而是將其通過throws拋出,那麼會有越來越多的函數受到影響。在這種情況下,我們要在多處對該Checked Exception進行處理。

如果在蔓延的過程中所遇到的是一個函數的重載或者接口的實現,那麼事情就會變得更加麻煩了。這是因爲一個函數聲明中的throws實際上是函數簽名的一部分。如果在函數重載或接口實現中添加了一個throws,那麼爲了保持原有的關係,被重載的函數或被實現的接口中的相應函數同樣需要添加一個throws聲明。而這樣的改動則會導致其它函數重載及接口實現同樣需要更改。

在上圖中,我們顯示了在一個接口聲明中添加throws的嚴重後果。在一開始,我們在應用中實現了接口函數Interface::method。此時在應用以及第三方應用中擁有六種對它的實現。但是如果A::method的實現中拋出了一個Checked Exception,那麼其就會要求接口中的相應函數也添加該throws聲明。一旦在接口中添加了throws聲明,那麼在應用以及第三方應用中的所有對該接口的實現都需要添加該throws聲明,即使在這些實現中並不存在可能拋出該異常的函數調用。

那麼我們應該怎麼解決這個問題呢?首先,我們應該儘早地對Checked Exception進行處理。這是因爲隨着Checked Exception沿着函數調用的軌跡向上傳遞的過程中,這些被拋出的Checked Exception的意義將逐漸模糊。例如在startupApplication函數中,我們可能需要讀取用戶的配置文件來根據用戶的原有偏好配置應用。由於該段邏輯需要讀取用戶的配置文件,因此其內部邏輯在運行時將可能拋出FileNotFoundException。如果這個FileNotFoundException沒有及時地被處理,那麼startupApplication函數的簽名將如下所示:

public void startupApplication throws FileNotFoundException {

……

}

在啓動一個應用的時候可能會產生一個FileNotFoundException異常?是的,這很容易理解,但是到底哪裏發生了異常?讀取偏好文件的時候還是加載Dll的時候?應用或用戶需要針對該異常進行什麼樣的處理?此時我們所能做的只能是通過分析該異常實例中所記錄的信息來判斷到底哪裏有異常。

反過來,如果我們在產生Checked Exception的時候立即對該異常進行處理,那麼此時我們將擁有有關該異常的最爲豐富的信息:

public void readPreference {

……

try {

FileReader fileReader = new FileReader(preferenceFile);

} catch(FileNotFoundException exception) {

// 在日誌中添加一條記錄並使用默認設置

}

……

}

但是在用戶那裏看來,他曾經所設置的偏好在這次使用時候已經不再有效了。這是我們的程序在運行時所產生的異常情況,因此我們需要通知用戶:因爲原來的偏好文件不再存在了,因此我們將使用默認的應用設置。而這一切則是通過一個在我們的應用中定義的RuntimeException類的派生類來完成的:

public void readPreference {

……

try {

FileReader fileReader = new FileReader(preferenceFile);

} catch(FileNotFoundException exception) {

logger.log(“Could not find user preference setting file: {0}”, preferenceFile);

throw ApplicationSpecificException(PREFERENCE_NOT_FOUND, exception);

}

……

}

可以看到,此時在catch塊中所拋出的ApplicationSpecificException異常中已經包含了足夠多的信息。這樣,我們的應用就可以通過捕獲ApplicationSpecificException來統一處理它們並將最爲詳盡的信息顯示給用戶,從而通知他因爲無法找到偏好文件而使用默認設置:

try {

startApplication;

} catch(ApplicationSpecificException exception) {

showWarningMessage(exception.getMessage);

}

手足無措的API使用者

另一種和Checked Exception相關的問題就是對它的隨意處理。在前面的講解中您或許已經知道了,如果一個Checked Exception不能在對API進行調用的函數中被處理,那麼該函數就需要添加throws聲明,從而導致多處代碼需要針對該Checked Exception進行修改。那麼好,爲了避免這種情況,我們就儘早地對它進行處理。但是在查看該API文檔的時候,我們卻發現文檔中並沒有添加任何有關該Checked Exception的詳細解釋:

/**

* ……

* throws SomeCheckedException

*/

public void someFunction throws SomeCheckedException {

……

}

而且我們也沒有辦法從該函數的簽名中看出到底爲什麼這個函數會拋出該異常,進而也不知道該異常是否需要對用戶可見。在這種情況下,我們只有截獲它並在日誌中添加一條記錄了事:

try {

someFunction;

} catch(SomeCheckedException exception) {

// 在日誌中添加一條記錄

}

很顯然,這並不是一種好的做法。而這一切的根本原因則是沒有說清楚到底爲什麼函數會拋出該Checked Exception。因此對於一個API編寫者而言,由於throws也是函數聲明的一部分,因此爲一個函數所能拋出的Checked Exception添加清晰準確的文檔實際上是非常重要的。

疲於應付的API用戶

除了沒有清晰的文檔之外,另一種讓API用戶非常抵觸的就是過度地對Checked Exception進行使用。

或許您已經接觸過類似的情況:一個類庫中用於取得數據的API,如getData(int index),通過throws拋出一個異常,以表示API用戶所傳入的參數index是一個非法值。可以想象得到的是,由於getData可能會被非常頻繁地使用,因此軟件開發人員需要在每一處調用都使用try … catch …塊來截獲該異常,從而使代碼顯得凌亂不堪。

如果一個類庫擁有一個這樣的API,那麼該類庫中的這種對Checked Exception的不恰當使用常常不止一個。那麼該類庫的這些API會大量地污染用戶代碼,使得這些用戶代碼中充斥着不必要也沒有任何意義的try…catch…塊,進而讓代碼邏輯顯得極爲晦澀難懂。

Record record = null;

try {

record = library.getDataAt(2);

} catch(InvalidIndexException exception) {

…… // 異常處理邏輯

}

record.setIntValue(record.getIntValue * 2);

try {

library.setDataAt(2, record);

} catch(InvalidIndexException exception) {

…… // 異常處理邏輯

}

反過來,如果這些都不是Checked Exception,而且軟件開發人員也能保證傳入的索引是合法的,那麼代碼會簡化很多:

Record record = library.getDataAt(2);

record.setIntValue(record.getIntValue * 2);

library.setDataAt(2, record);

那麼我們應該在什麼時候使用Checked Exception呢?就像前面所說的,如果一個異常所表示的並不是代碼本身的不足所導致的非正常狀態,而是一系列應用本身也無法控制的情況,那麼我們將需要使用Checked Exception。就以前面所列出的FileReader類的構造函數爲例:

public FileReader(String fileName) throws FileNotFoundException

該構造函數的簽名所表示的意義實際上是:

必須通過傳入的參數fileName來標示需要打開的文件 如果文件存在,那麼該構造函數將返回一個FileReader類的實例 對該構造函數進行使用的代碼必須處理由fileName所標示的文件不存在,進而拋出FileNotFoundException的情況 也就是說,Checked Exception實際上是API設計中的一部分。在調用這個API的時候,你不得不處理目標文件不存在的情況。而這則是由文件系統的自身特性所導致的。而之所以Checked Exception導致瞭如此多的爭論和誤用,更多是因爲我們在用異常這個用來表示應用中的運行錯誤這個語言組成來通知用戶他所必須處理的應用無法控制的可能情況。也就是說,其爲異常賦予了新的含義,使得異常需要表示兩個完全不相干的概念。而在沒有仔細分辨的情況下,這兩個概念是極容易混淆的。因此在嘗試着定義一個Checked Exception之前,API編寫者首先要考慮這個異常所表示的到底是系統自身缺陷所導致的運行錯誤,還是要讓用戶自己來處理的邊緣情況。

正確地使用Checked Exception

實際上,如何正確地使用Checked Exception已經在前面的各章節講解中進行了詳細地說明。在這裏我們再次做一個總結,同時也用來加深一下印象。

從API編寫者的角度來講,他所需要考慮的就是在何時使用一個Checked Exception。

首先,Checked Exception應當只在異常情況對於API以及API的使用者都無法避免的情況下被使用。例如在打開一個文件的時候,API以及API的使用者都沒有辦法保證該文件一定存在。反過來,在通過索引訪問數據的時候,如果API的使用者對參數index傳入的是-1,那麼這就是一個代碼上的錯誤,是完全可以避免的。因此對於index參數值不對的情況,我們應該使用Unchecked Exception。

其次,Checked Exception不應該被廣泛調用的API所拋出。這一方面是基於代碼整潔性的考慮,另一方面則是因爲Checked Exception本身的實際意義是API以及API的使用者都無法避免的情況。如果一個應用有太多處這種“無法避免的異常”,那麼這個程序是否擁有足夠的質量也是一個很值得考慮的問題。而就API提供者而言,在一個主要的被廣泛使用的功能上拋出這種異常,也是對其自身API的一種否定。

再次,一個Checked Exception應該有明確的意義。這種明確意義的標準則是需要讓API使用者能夠看到這個Checked Exception所對應的異常類,該異常類所包含的各個域,並閱讀相應的API文檔以後就能夠瞭解到底哪裏出現了問題,進而向用戶提供準確的有關該異常的解釋。

而對於API的用戶而言,一旦遇到了一個API會拋出Checked Exception,那麼他就需要考慮使用一個Wrapped Exception來將該Checked Exception包裝起來。那什麼是Wrapped Exception呢?

簡單地說,Wrapped Exception就是將一個異常包裝起來的異常。在try…catch…塊捕獲到一個異常的時候,該異常內部所記錄的消息可能並不合適。就以前面我們已經舉過的加載偏好的示例爲例。在啓動時,應用會嘗試讀取用戶的偏好設置。這些偏好設置記錄在了一個文件中,卻可能已經被誤刪除。在這種情況下,對該偏好文件的讀取會導致一個FileNotFoundException拋出。但是在該異常中所記錄的信息對於用戶,甚至應用編寫者而言沒有任何價值:“Could not find file preference.xml while opening file”。在這種情況下,我們就需要構造一個新的異常,在該異常中標示準確的錯誤信息,並將FileNotFoundException作爲新異常的原因:

public void readPreference {

……

try {

FileReader fileReader = new FileReader(preferenceFile);

} catch(FileNotFoundException exception) {

logger.log(“Could not find user preference setting file: {0}” preferenceFile);

throw ApplicationSpecificException(PREFERENCE_NOT_FOUND, exception);

}

……

}

上面的示例代碼中重新拋出了一個ApplicationSpecificException類型的異常。從它的名字就可以看出,其應該是API使用者在應用實現中所添加的應用特有的異常。爲了避免調用棧中的每一個函數都需要添加throws聲明,該異常需要從RuntimeException派生。這樣應用就可以通過在調用棧的最底層捕捉這些異常並對這些異常進行處理:在系統日誌中添加一條異常記錄,只對用戶顯示異常中的消息,以防止異常內部的調用棧信息暴露過多的實現細節等:

try {

……

} catch(ApplicationSpecificException exception) {

logger.log(exception.getLevel, exception.getMessage, exception);

// 將exception內部記錄的信息顯示給用戶(或添加到請求的響應中返回)

// 如showWarningMessage(exception.getMessage);

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章