天天看點

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

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

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異常簡介

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

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

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來強制軟體開發人員在編寫代碼的時候就考慮對這些無法避免的情況的處理,進而提高代碼品質。

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

天使變惡魔

  既然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會非常快速地蔓延開來:

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

  從上圖中已經可以看出:如果不去處理Checked Exception,而是将其通過throws抛出,那麼會有越來越多的函數受到影響。在這種情況下,我們要在多處對該Checked Exception進行處理。

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

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

  在上圖中,我們顯示了在一個接口聲明中添加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

商業轉載請事先與我聯系:[email protected]