Java異常:選擇Checked Exception還是Unchecked Exception?

 分類:

目錄(?)[+]

Java包含兩種異常:checked異常unchecked異常。C#只有unchecked異常。checked和unchecked異常之間的區別是:

  1. Checked異常必須被顯式地捕獲或者傳遞,如Basic try-catch-finally Exception Handling一文中所說。而unchecked異常則可以不必捕獲或拋出。
  2. Checked異常繼承java.lang.Exception類。Unchecked異常繼承自java.lang.RuntimeException類。

有許多支持或者反對二者甚至是否應該使用checked異常的爭論。本文將討論一些常見的觀點。開始之前,先澄清一個問題:

Checked和unchecked異常從功能的角度來講是等價的。可以用checked異常實現的功能必然也可以用unchecked異常實現,反之亦然。

選擇checked異常還是unchecked異常是個人習慣或者組織規定問題。並不存在誰比誰強大的問題。

一個簡單的例子

在討論checked和unchecked異常的優缺點前先看一下代碼中如下使用它們。下面是一個拋出checked異常的方法,另一個方法調用了它:

[java] view plain copy
  1. public void storeDataFromUrl(String url){  
  2.     try {  
  3.         String data = readDataFromUrl(url);  
  4.     } catch (BadUrlException e) {  
  5.         e.printStackTrace();  
  6.     }  
  7. }  
  8.   
  9. public String readDataFromUrl(String url)  
  10. throws BadUrlException{  
  11.     if(isUrlBad(url)){  
  12.         throw new BadUrlException("Bad URL: " + url);  
  13.     }  
  14.   
  15.     String data = null;  
  16.     //read lots of data over HTTP and return  
  17.     //it as a String instance.  
  18.   
  19.     return data;  
  20. }  
readDataFromUrl()方法拋出了BadUrlException。BadUrlException是我自己實現的一個類。由於BadUrlException繼承自java.lang.Exception,因而它是checked異常:

[java] view plain copy
  1. public class BadUrlException extends Exception {  
  2.     public BadUrlException(String s) {  
  3.         super(s);  
  4.     }  
  5. }  
如果storeDataFromUrl()方法想要調用readDataFromUrl(),它只有兩種選擇。要麼捕獲BadUrlException,要麼沿着調用棧繼續向上傳播該異常。上面的代碼中storeDataFromUrl() 捕獲了異常。向上傳播異常的實現方法如下:

[java] view plain copy
  1. public void storeDataFromUrl(String url)  
  2. throws BadUrlException{  
  3.     String data = readDataFromUrl(url);  
  4. }  
可以看到,上述代碼去掉了catch塊,方法聲明中加上了throws BadUrlException。下面,討論一下unchecked異常的實現方法。首先,將BadUrlException改爲繼承自java.lang.RuntimeException:

[java] view plain copy
  1. public class BadUrlException extends RuntimeException {  
  2.     public BadUrlException(String s) {  
  3.         super(s);  
  4.     }  
  5. }  
然後,把方法中的異常改爲unchecked BadUrlException:

[java] view plain copy
  1. public void storeDataFromUrl(String url){  
  2.     String data = readDataFromUrl(url);  
  3. }  
  4.   
  5. public String readDataFromUrl(String url) {  
  6.     if(isUrlBad(url)){  
  7.         throw new BadUrlException("Bad URL: " + url);  
  8.     }  
  9.   
  10.     String data = null;  
  11.     //read lots of data over HTTP and  
  12.     //return it as a String instance.  
  13.   
  14.     return data;  
  15. }  
注意,readDataFromUrl()方法不再聲明拋出BadUrlException。storeDataFromUrl()方法也不必捕獲BadUrlException。storeDataFromUrl()也可以捕獲異常,但不再是必須的了,而且它也不必聲明傳播異常。

Checked 還是Unchecked?

上一節我們已經討論了checked異常和unchecked異常代碼實現上的區別,下面深入分析二者的適用情況(支持和反對二者的觀點)。

一些Java書籍(如Suns Java Tutorial)中建議在遇到可恢復的錯誤時採用checked異常,遇到不可恢復的異常時採用unchecked異常。事實上,大多數應用必須從幾乎所有異常(包括NullPointerException,IllegalArgumentException和許多其他unchecked異常)中恢復。執行失敗的action/transaction會被取消,但是應用程序必須能繼續處理後續的action或transaction。關閉一個應用的唯一合法時機是應用程序啓動時。例如,如果配置文件丟失而且應用程序依賴於它,那麼這時關閉應用程序是合法的。

我建議的使用策略是:選擇checked異常或unchecked異常中的一種使用。混合使用經常導致混亂和不一致。如果你是一個經驗豐富的程序員,那麼根據自己的需要使用吧。

下面是支持和反對checked/unchecked異常的一些最常見的觀點。支持一種類型的exception的觀點通常意味着反對另一種(支持checked = 反對unchecked,支持unchecked = 反對checked)。因此,只列出了支持checked異常或unchecked異常的列表。

  1. 支持Checked異常:
    編譯器強制檢查,checked異常必須被捕獲或者傳播,這樣就不會忘記處理異常。
  2. 支持Checked異常:
    Unchecked異常容易忘記處理,由於編譯器不強制程序員捕獲或傳播它(第一條的反面表述)。
  3. 支持Unchecked異常:
    沿調用棧向上傳播的Checked異常破壞了頂層的方法,因爲這些方法必須聲明拋出所有它們調用的方法拋出的異常。
  4. 支持Checked異常:
    當方法不聲明它們會拋出何種異常時,就難以處理它們拋出的異常。
  5. 支持Unchecked異常:
    Check異常的拋出作爲方法接口的一部分,這使得添加或移除早期版本中方法的異常難以實現。

上述每一個觀點都有相反的觀點,下面我會詳細討論這些觀點。

觀點1(支持Checked異常):

編譯器強制檢查,checked異常必須被捕獲或者傳播,這樣就不會忘記處理異常。

相反觀點:

當被強制捕獲或傳播許多異常時,開發人員的效率會受到影響,也可能會只寫

[java] view plain copy
  1. try{  
  2.    callMethodThatThrowsException();  
  3. catch(Exception e){  
  4. }  
來忽略錯誤(糊弄了事)。

觀點2(支持Checked異常):

Unchecked異常容易忘記處理,由於編譯器不強制程序員捕獲或傳播它(第一條的反面表述)。

相反觀點1:

強制處理或傳播checked異常導致的草率地異常處理非常糟糕。

相反觀點2:

在近期的一個大型項目中我們決定採用unchecked異常。我在這個項目中獲得的經驗是:使用unchecked異常時,任何方法都可能拋出異常。因此我不論在寫哪一部分代碼都時刻注意異常。而不只是聲明瞭checked異常的地方。

此外,許多沒有聲明任何checked異常的標準的Java API方法會拋出諸如NullPointerException或者InvalidArgumentException之類的unchecked異常。你的應用程序需要處理這些unchecked異常。你可能會說checked異常的存在讓我們容易忘記處理unchecked異常,因爲unchecked異常沒有顯式地聲明。

觀點3(支持Unchecked異常):

沿調用棧向上傳播的Checked異常破壞了頂層的方法,因爲這些方法必須聲明拋出所有它們調用的方法拋出的異常。即,聲明的異常聚合了調用棧中所有的方法拋出的異常。例如:

[java] view plain copy
  1. public long readNumberFromUrl(String url)  
  2. throws BadUrlExceptions, BadNumberException{  
  3.     String data = readDataFromUrl(url);  
  4.     long number = convertData(data);  
  5.     return number;  
  6. }  
  7.   
  8. private String readDataFromUrl(String url)  
  9. throws BadUrlException {  
  10.    //throw BadUrlException if url is bad.  
  11.    //read data and return it.  
  12. }  
  13.   
  14. private long convertData(String data)  
  15. throws BadNumberException{  
  16.     //convert data to long.  
  17.     //throw BadNumberException if number isn't within valid range.  
  18. }  
readNumberFromUrl()必須聲明拋出BadUrlException和BadNumberException,而這兩個異常是readNumberFromUrl()調用的readDataFromUrl() 和 converData()方法拋出的異常。可以想象一個有數千個類的應用程序的頂層方法需要聲明多少異常。這使得checked異常傳播是一件非常痛苦的事。

相反觀點1:

異常聲明傳播聚合在實際應用程序中很少發生。開發人員時常使用異常包裝機制來優化。如下:

[java] view plain copy
  1. public void readNumberFromUrl(String url)  
  2. throws ApplicationException{  
  3.     try{  
  4.         String data = readDataFromUrl(url);  
  5.         long number = convertData(data);  
  6.     } catch (BadUrlException e){  
  7.         throw new ApplicationException(e);  
  8.     } catch (BadNumberException e){  
  9.         throw new ApplicationException(e);  
  10.     }  
  11. }  
readNumberFromUrl()方法只需要聲明拋出ApplicationException即可。BadUrlException和BadNumberException被捕獲幷包裝進一個更通用的ApplicationException中。通過異常包裝就可以避免異常聲明聚合。

我的個人觀點是,如果你只是包裝異常但並不提供更多信息,那爲什麼要包裝它呢?try-catch塊就成了多餘的代碼,沒有做任何有意義的事。只需將ApplicationException,BadUrlException和BadNumberException定義爲unchecked異常。下面是上述代碼的unchecked版本:

[java] view plain copy
  1. public void readNumberFromUrl(String url){  
  2.     String data = readDataFromUrl(url);  
  3.     long number = convertData(data);  
  4. }  
也可以包裝unchecked異常。下面是unchecked代碼的包裝版本。注意readNumberFromUrl()方法不聲明拋出ApplicationException,即使它可能拋出該異常。

[java] view plain copy
  1. public void readNumberFromUrl(String url)  
  2.     try{  
  3.         String data = readDataFromUrl(url);  
  4.         long number = convertData(data);  
  5.     } catch (BadUrlException e){  
  6.         throw new ApplicationException(  
  7.             "Error reading number from URL", e);  
  8.     } catch (BadNumberException e){  
  9.         throw new ApplicationException(  
  10.             "Error reading number from URL", e);  
  11.     }  
  12. }  

相反觀點2:

另一種常用於避免異常聲明聚集的技術是創建一個應用程序基礎異常類。應用程序中拋出的所有異常必須是基礎異常類的子類。所有拋出異常的方法只需聲明拋出基礎異常。比如一個拋出Exception的方法可能拋出Exception的任何子類。如下代碼:

[java] view plain copy
  1. public long readNumberFromUrl(String url)  
  2. throws ApplicationException {  
  3.     String data = readDataFromUrl(url);  
  4.     long number = convertData(data);  
  5.     return number;  
  6. }  
  7.   
  8. private String readDataFromUrl(String url)  
  9. throws BadUrlException {  
  10.    //throw BadUrlException if url is bad.  
  11.    //read data and return it.  
  12. }  
  13.   
  14. private long convertData(String data)  
  15. throws BadNumberException{  
  16.     //convert data to long.  
  17.     //throw BadNumberException if number isn't within valid range.  
  18. }  
  19.   
  20.   
  21. public class ApplicationException extends Exception{ }  
  22. public class BadNumberException   extends ApplicationException{}  
  23. public class BadUrlException      extends ApplicationException{}  
注意BadNumberException和BadUrlException不再被聲明拋出,也不再被捕獲,也沒有包裝。它們是ApplicationException的子類,因此它們會沿着調用棧向上傳播。

我還是支持異常包裝:如果應用程序的所有方法都聲明拋出ApplicationException(基礎異常),爲什麼不直接將ApplicationException定義爲unchecked?這樣不但省去了一些try-catch塊,也省去了throws語句。

觀點4(支持Checked異常):

當方法不聲明它們會拋出何種異常時,就難以處理它們拋出的異常。如果沒有聲明,你就不會知道方法會拋出什麼樣的異常。因此你也就不會知道如何處理它們。當然,如果你能訪問源代碼,就不存在這個問題,因爲你可以從源代碼中看出來會拋出何種異常。

相反觀點:

在多數情況下,處理異常的措施僅僅是向用戶彈出一個錯誤提示消息,將錯誤消息寫入日誌,回滾事務等。無論發生何種異常,你可能會採用相同的處理措施。因此,應用程序通常包含一些集中的通用錯誤處理代碼。如此一來,確切獲知拋出了何種異常也就不那麼重要了。

觀點5(支持Unchecked異常):

Check異常的拋出作爲方法接口的一部分,這使得添加或移除早期版本中方法的異常難以實現。

相反觀點:

如果方法採用了基礎異常機制,就不存在這個問題。如果方法聲明拋出基礎異常,那麼可以方便拋出新異常。唯一的需求是新異常必須是基礎異常的子類。

需要再強調一遍的是,讓所有可能拋出異常的方法聲明拋出相同的基礎異常的意義何在?這樣能比拋出unchecked異常更好地處理異常嗎?


總結

我過去支持checked異常,但是最近我改變了我的觀點。Rod Johnson(spring Framework),Anders Hejlsberg(C#之父),Joshua Bloch(Effective Java,條目41:避免checked異常的不必要的使用)和其他一些朋友使我重新考慮了checked異常的真實價值。最近我們嘗試在一個較大的項目中使用unchecked異常,效果還不錯。錯誤處理被集中在了少數幾個類中。會有需要本地錯誤處理的地方,而不是將異常傳播給主錯誤處理代碼。但是這種地方不會很多。由於代碼中不會到處都是try-catch塊,我們的代碼變得可讀性更好。換句話說,使用unchecked異常比使用checked異常減少了無用的catch-rethrow try-catch塊。總之,我建議使用unchecked異常。至少在一個工程中嘗試過。我總結了以下原因:

  • Unchecked異常不會使代碼顯得雜亂,因爲其避免了不必要的try-catch塊。
  • Unchecked異常不會因爲異常聲明聚集使方法聲明顯得雜亂。
  • 關於容易忘記處理unchecked異常的觀點在我的實踐中沒有發生。
  • 關於無法獲知如何處理未聲明異常的觀點在我的實踐中沒有發生。
  • Unchecked異常避免了版本問題。

你的項目中使用何種異常由你自己決定。下面是相關的資料。

Anders Hejlsberg on checked vs. unchecked exceptions
http://www.artima.com/intv/handcuffs.html 

James Gosling on checked exceptions
http://www.artima.com/intv/solid.html 

Bill Venners on Exceptions
http://www.artima.com/interfacedesign/exceptions.html 

Bruce Eckel on checked exceptions
http://www.artima.com/intv/typingP.html 

Designing with Exceptions (Bill Venners - www.artima.com)
http://www.artima.com/designtechniques/desexcept.html 

Effective Java (Joshua Bloch - Addison Wesley 2001) 

Daniel Pietraru - in favor of checked exceptions
http://littletutorials.com/2008/05/06/exceptional-java-checked-exceptions-are-priceless-for-everything-else-there-is-the-the-runtimeexception/


英文原文:http://tutorials.jenkov.com/java-exception-handling/checked-or-unchecked-exceptions.html

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