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

來源:互聯網
上載者:User

標籤:

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

1 public void shouldNotThrowCheckedException() {2     // 該API調用可能拋出一個不明原因的Checked Exception3     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); // 可能拋出FileNotFoundException3     ……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); // 可能拋出FileNotFoundException3     ……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 SomeCheckedException4  */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]

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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.