Java中的Checked Exception——美麗世界中潛藏的惡魔?

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

1 public void shouldNotThrowCheckedException() {
2     // 該API調用可能拋出一個不明緣由的Checked Exception
3     exceptionalAPI();
4 }

  不然Java編譯器會因爲shouldNotThrowCheckedException()函數沒有聲明其可能拋出的Checked Exception而報錯。可是若是經過throws標明瞭該函數所可能拋出的Checked Exception,那麼其它對shouldNotThrowCheckedException()函數的調用一樣須要經過throws標明其可能拋出該Checked Exception。數組

  哦,這可真是一件使人煩燥的事情。那咱們應該如何對這些Checked Exception進行處理呢?在本文中,咱們將對如何在Java應用中使用及處理Checked Exception進行簡單地介紹。網絡

 

Java異常簡介app

  在詳細介紹Checked Exception所致使的問題以前,咱們先用一小段篇幅簡單介紹一下Java中的異常。函數

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

1 public FileReader(String fileName) throws FileNotFoundException {
2     ……
3 }

  那麼在調用該構造函數的函數中,咱們須要經過try…catch…來處理該異常:設計

1 public void processFile() {
2     try {
3         FileReader fileReader = new FileReader(inFile);
4     } catch(FileNotFoundException exception) {
5         // 異常處理邏輯
6     }
7     ……
8 }

  若是咱們不經過try…catch…來處理該異常,那麼咱們就不得不在函數聲明中經過throws標明該函數會拋出FileNotFoundException:日誌

1 public void processFile() throws FileNotFoundException {
2     FileReader fileReader = new FileReader(inFile); // 可能拋出FileNotFoundException
3     ……
4 }

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

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

 

天使變惡魔

  既然Java中的Checked Exception可以提升用戶代碼質量,爲何還有那麼多人反對它呢?緣由很簡單:它太容易被誤用了。而在本節中,咱們就將列出這些誤用狀況並提出相應的網絡上最爲推薦的解決方案。

 

無處不在的throws

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

1 public void processFile() throws FileNotFoundException {
2     FileReader fileReader = new FileReader(inFile); // 可能拋出FileNotFoundException
3     ……
4 }

  而在調用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()函數的簽名將以下所示:

1 public void startupApplication() throws FileNotFoundException {
2     ……
3 }

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

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

1 public void readPreference() {
2     ……
3     try {
4         FileReader fileReader = new FileReader(preferenceFile);
5     } catch(FileNotFoundException exception) {
6         // 在日誌中添加一條記錄並使用默認設置
7     }
8     ……
9 }

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

 1 public void readPreference() {
 2     ……
 3     try {
 4         FileReader fileReader = new FileReader(preferenceFile);
 5     } catch(FileNotFoundException exception) {
 6         logger.log(「Could not find user preference setting file: {0}」 preferenceFile);
 7         throw ApplicationSpecificException(PREFERENCE_NOT_FOUND, exception);
 8     }
 9     ……
10 }

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

1 try {
2     startApplication();
3 } catch(ApplicationSpecificException exception) {
4     showWarningMessage(exception.getMessage());
5 }

 

手足無措的API使用者

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

1 /**
2  * ……
3  * throws SomeCheckedException
4  */
5 public void someFunction() throws SomeCheckedException {
6 }

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

1 try {
2     someFunction();
3 } catch(SomeCheckedException exception) {
4     // 在日誌中添加一條記錄
5 }

                很顯然,這並非一種好的作法。而這一切的根本緣由則是沒有說清楚到底爲何函數會拋出該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…塊,進而讓代碼邏輯顯得極爲晦澀難懂。

 1 Record record = null;
 2 try {
 3     record = library.getDataAt(2);
 4 } catch(InvalidIndexException exception) {
 5     …… // 異常處理邏輯
 6 }
 7 record.setIntValue(record.getIntValue() * 2);
 8 try {
 9     library.setDataAt(2, record);
10 } catch(InvalidIndexException exception) {
11     …… // 異常處理邏輯
12 }

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

1 Record record = library.getDataAt(2);
2 record.setIntValue(record.getIntValue() * 2);
3 library.setDataAt(2, record);

  那麼咱們應該在何時使用Checked Exception呢?就像前面所說的,若是一個異常所表示的並非代碼自己的不足所致使的非正常狀態,而是一系列應用自己也沒法控制的狀況,那麼咱們將須要使用Checked Exception。就之前面所列出的FileReader類的構造函數爲例:

1 public FileReader(String fileName) throws FileNotFoundException

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

  1. 必須經過傳入的參數fileName來標示須要打開的文件
  2. 若是文件存在,那麼該構造函數將返回一個FileReader類的實例
  3. 對該構造函數進行使用的代碼必須處理由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做爲新異常的緣由:

 1 public void readPreference() {
 2     ……
 3     try {
 4         FileReader fileReader = new FileReader(preferenceFile);
 5     } catch(FileNotFoundException exception) {
 6         logger.log(「Could not find user preference setting file: {0}」 preferenceFile);
 7         throw ApplicationSpecificException(PREFERENCE_NOT_FOUND, exception);
 8     }
 9     ……
10 }

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

1 try {
2     ……
3 } catch(ApplicationSpecificException exception) {
4     logger.log(exception.getLevel(), exception.getMessage(), exception);
5     // 將exception內部記錄的信息顯示給用戶(或添加到請求的響應中返回)
6     // 如showWarningMessage(exception.getMessage());
7 }

 

轉載請註明原文地址並標明轉載:http://www.cnblogs.com/loveis715/p/4596551.html

商業轉載請事先與我聯繫:silverfox715@sina.com

相關文章
相關標籤/搜索