Java中的Checked Exception——美麗世界中潛藏的惡魔?
在使用Java編寫應(yīng)用的時(shí)候,我們常常需要通過第三方類庫來幫助我們完成所需要的功能。有時(shí)候這些類庫所提供的很多API都通過throws聲明了它們所可能拋出的異常。但是在查看這些API的文檔時(shí),我們卻沒有辦法找到有關(guān)這些異常的詳盡解釋。在這種情況下,我們不能簡(jiǎn)單地忽略這些由throws所聲明的異常:
1 public void shouldNotThrowCheckedException() { 2 // 該API調(diào)用可能拋出一個(gè)不明原因的Checked Exception 3 exceptionalAPI(); 4 }
否則Java編譯器會(huì)由于shouldNotThrowCheckedException()函數(shù)沒有聲明其可能拋出的Checked Exception而報(bào)錯(cuò)。但是如果通過throws標(biāo)明了該函數(shù)所可能拋出的Checked Exception,那么其它對(duì)shouldNotThrowCheckedException()函數(shù)的調(diào)用同樣需要通過throws標(biāo)明其可能拋出該Checked Exception。
哦,這可真是一件令人煩燥的事情。那我們應(yīng)該如何對(duì)這些Checked Exception進(jìn)行處理呢?在本文中,我們將對(duì)如何在Java應(yīng)用中使用及處理Checked Exception進(jìn)行簡(jiǎn)單地介紹。
Java異常簡(jiǎn)介
在詳細(xì)介紹Checked Exception所導(dǎo)致的問題之前,我們先用一小段篇幅簡(jiǎn)單介紹一下Java中的異常。
在Java中,異常主要分為三種:Exception,RuntimeException以及Error。這三類異常都是Throwable的子類。直接從Exception派生的各個(gè)異常類型就是我們剛剛提到的Checked Exception。它的一個(gè)比較特殊的地方就是強(qiáng)制調(diào)用方對(duì)該異常進(jìn)行處理。就以我們常見的用于讀取一個(gè)文件內(nèi)容的FileReader類為例。在該類的構(gòu)造函數(shù)聲明中聲明了其可能會(huì)拋出FileNotFoundException:
1 public FileReader(String fileName) throws FileNotFoundException { 2 …… 3 }
那么在調(diào)用該構(gòu)造函數(shù)的函數(shù)中,我們需要通過try…catch…來處理該異常:
1 public void processFile() { 2 try { 3 FileReader fileReader = new FileReader(inFile); 4 } catch(FileNotFoundException exception) { 5 // 異常處理邏輯 6 } 7 …… 8 }
如果我們不通過try…catch…來處理該異常,那么我們就不得不在函數(shù)聲明中通過throws標(biāo)明該函數(shù)會(huì)拋出FileNotFoundException:
1 public void processFile() throws FileNotFoundException { 2 FileReader fileReader = new FileReader(inFile); // 可能拋出FileNotFoundException 3 …… 4 }
而RuntimeException類的各個(gè)派生類則沒有這種強(qiáng)制調(diào)用方對(duì)異常進(jìn)行處理的需求。為什么這兩種異常會(huì)有如此大的區(qū)別呢?因?yàn)镽untimeException所表示的是軟件開發(fā)人員沒有正確地編寫代碼所導(dǎo)致的問題,如數(shù)組訪問越界等。而派生自Exception類的各個(gè)異常所表示的并不是代碼本身的不足所導(dǎo)致的非正常狀態(tài),而是一系列應(yīng)用本身也無法控制的情況。例如一個(gè)應(yīng)用在嘗試打開一個(gè)文件并寫入的時(shí)候,該文件已經(jīng)被另外一個(gè)應(yīng)用打開從而無法寫入。對(duì)于這些情況,Java通過Checked Exception來強(qiáng)制軟件開發(fā)人員在編寫代碼的時(shí)候就考慮對(duì)這些無法避免的情況的處理,從而提高代碼質(zhì)量。
而Error則是一系列很難通過程序解決的問題。這些問題基本上是無法恢復(fù)的,例如內(nèi)存空間不足等。在這種情況下,我們基本無法使得程序重新回到正常軌道上。因此一般情況下,我們不會(huì)對(duì)從Error類派生的各個(gè)異常進(jìn)行處理。而且由于其實(shí)際上與本文無關(guān),因此我們不再對(duì)其進(jìn)行詳細(xì)講解。
天使變惡魔
既然Java中的Checked Exception能夠提高用戶代碼質(zhì)量,為什么還有那么多人反對(duì)它呢?原因很簡(jiǎn)單:它太容易被誤用了。而在本節(jié)中,我們就將列出這些誤用情況并提出相應(yīng)的網(wǎng)絡(luò)上最為推薦的解決方案。
無處不在的throws
第一種誤用的情況就是Checked Exception的廣泛傳播。在前面已經(jīng)提到過,調(diào)用一個(gè)可能拋出Checked Exception的API時(shí),軟件開發(fā)人員可以有兩種選擇。其中一種選擇就是在對(duì)該API進(jìn)行調(diào)用的函數(shù)上添加throws聲明,并將該Checked Exception向上傳遞:
1 public void processFile() throws FileNotFoundException { 2 FileReader fileReader = new FileReader(inFile); // 可能拋出FileNotFoundException 3 …… 4 }
而在調(diào)用processFile()函數(shù)的代碼中,軟件開發(fā)人員可能覺得這里還不是處理異常FileNotFoundException的合適地點(diǎn),因此他通過throws將該異常再次向上傳遞。但是在一個(gè)函數(shù)上添加throws意味著其它對(duì)該函數(shù)進(jìn)行調(diào)用的代碼同樣需要處理該throws聲明。在一個(gè)代碼復(fù)用性比較好的系統(tǒng)中,這些throws會(huì)非常快速地蔓延開來:

從上圖中已經(jīng)可以看出:如果不去處理Checked Exception,而是將其通過throws拋出,那么會(huì)有越來越多的函數(shù)受到影響。在這種情況下,我們要在多處對(duì)該Checked Exception進(jìn)行處理。
如果在蔓延的過程中所遇到的是一個(gè)函數(shù)的重載或者接口的實(shí)現(xiàn),那么事情就會(huì)變得更加麻煩了。這是因?yàn)橐粋€(gè)函數(shù)聲明中的throws實(shí)際上是函數(shù)簽名的一部分。如果在函數(shù)重載或接口實(shí)現(xiàn)中添加了一個(gè)throws,那么為了保持原有的關(guān)系,被重載的函數(shù)或被實(shí)現(xiàn)的接口中的相應(yīng)函數(shù)同樣需要添加一個(gè)throws聲明。而這樣的改動(dòng)則會(huì)導(dǎo)致其它函數(shù)重載及接口實(shí)現(xiàn)同樣需要更改:

在上圖中,我們顯示了在一個(gè)接口聲明中添加throws的嚴(yán)重后果。在一開始,我們?cè)趹?yīng)用中實(shí)現(xiàn)了接口函數(shù)Interface::method()。此時(shí)在應(yīng)用以及第三方應(yīng)用中擁有六種對(duì)它的實(shí)現(xiàn)。但是如果A::method()的實(shí)現(xiàn)中拋出了一個(gè)Checked Exception,那么其就會(huì)要求接口中的相應(yīng)函數(shù)也添加該throws聲明。一旦在接口中添加了throws聲明,那么在應(yīng)用以及第三方應(yīng)用中的所有對(duì)該接口的實(shí)現(xiàn)都需要添加該throws聲明,即使在這些實(shí)現(xiàn)中并不存在可能拋出該異常的函數(shù)調(diào)用。
那么我們應(yīng)該怎么解決這個(gè)問題呢?首先,我們應(yīng)該盡早地對(duì)Checked Exception進(jìn)行處理。這是因?yàn)殡S著Checked Exception沿著函數(shù)調(diào)用的軌跡向上傳遞的過程中,這些被拋出的Checked Exception的意義將逐漸模糊。例如在startupApplication()函數(shù)中,我們可能需要讀取用戶的配置文件來根據(jù)用戶的原有偏好配置應(yīng)用。由于該段邏輯需要讀取用戶的配置文件,因此其內(nèi)部邏輯在運(yùn)行時(shí)將可能拋出FileNotFoundException。如果這個(gè)FileNotFoundException沒有及時(shí)地被處理,那么startupApplication()函數(shù)的簽名將如下所示:
1 public void startupApplication() throws FileNotFoundException { 2 …… 3 }
在啟動(dòng)一個(gè)應(yīng)用的時(shí)候可能會(huì)產(chǎn)生一個(gè)FileNotFoundException異常?是的,這很容易理解,但是到底哪里發(fā)生了異常?讀取偏好文件的時(shí)候還是加載Dll的時(shí)候?應(yīng)用或用戶需要針對(duì)該異常進(jìn)行什么樣的處理?此時(shí)我們所能做的只能是通過分析該異常實(shí)例中所記錄的信息來判斷到底哪里有異常。
反過來,如果我們?cè)诋a(chǎn)生Checked Exception的時(shí)候立即對(duì)該異常進(jìn)行處理,那么此時(shí)我們將擁有有關(guān)該異常的最為豐富的信息:
1 public void readPreference() { 2 …… 3 try { 4 FileReader fileReader = new FileReader(preferenceFile); 5 } catch(FileNotFoundException exception) { 6 // 在日志中添加一條記錄并使用默認(rèn)設(shè)置 7 } 8 …… 9 }
但是在用戶那里看來,他曾經(jīng)所設(shè)置的偏好在這次使用時(shí)候已經(jīng)不再有效了。這是我們的程序在運(yùn)行時(shí)所產(chǎn)生的異常情況,因此我們需要通知用戶:因?yàn)樵瓉淼钠梦募辉俅嬖诹耍虼宋覀儗⑹褂媚J(rèn)的應(yīng)用設(shè)置。而這一切則是通過一個(gè)在我們的應(yīng)用中定義的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 }
可以看到,此時(shí)在catch塊中所拋出的ApplicationSpecificException異常中已經(jīng)包含了足夠多的信息。這樣,我們的應(yīng)用就可以通過捕獲ApplicationSpecificException來統(tǒng)一處理它們并將最為詳盡的信息顯示給用戶,從而通知他因?yàn)闊o法找到偏好文件而使用默認(rèn)設(shè)置:
1 try { 2 startApplication(); 3 } catch(ApplicationSpecificException exception) { 4 showWarningMessage(exception.getMessage()); 5 }
手足無措的API使用者
另一種和Checked Exception相關(guān)的問題就是對(duì)它的隨意處理。在前面的講解中您或許已經(jīng)知道了,如果一個(gè)Checked Exception不能在對(duì)API進(jìn)行調(diào)用的函數(shù)中被處理,那么該函數(shù)就需要添加throws聲明,從而導(dǎo)致多處代碼需要針對(duì)該Checked Exception進(jìn)行修改。那么好,為了避免這種情況,我們就盡早地對(duì)它進(jìn)行處理。但是在查看該API文檔的時(shí)候,我們卻發(fā)現(xiàn)文檔中并沒有添加任何有關(guān)該Checked Exception的詳細(xì)解釋:
1 /** 2 * …… 3 * throws SomeCheckedException 4 */ 5 public void someFunction() throws SomeCheckedException { 6 }
而且我們也沒有辦法從該函數(shù)的簽名中看出到底為什么這個(gè)函數(shù)會(huì)拋出該異常,進(jìn)而也不知道該異常是否需要對(duì)用戶可見。在這種情況下,我們只有截獲它并在日志中添加一條記錄了事:
1 try { 2 someFunction(); 3 } catch(SomeCheckedException exception) { 4 // 在日志中添加一條記錄 5 }
很顯然,這并不是一種好的做法。而這一切的根本原因則是沒有說清楚到底為什么函數(shù)會(huì)拋出該Checked Exception。因此對(duì)于一個(gè)API編寫者而言,由于throws也是函數(shù)聲明的一部分,因此為一個(gè)函數(shù)所能拋出的Checked Exception添加清晰準(zhǔn)確的文檔實(shí)際上是非常重要的。
疲于應(yīng)付的API用戶
除了沒有清晰的文檔之外,另一種讓API用戶非常抵觸的就是過度地對(duì)Checked Exception進(jìn)行使用。
或許您已經(jīng)接觸過類似的情況:一個(gè)類庫中用于取得數(shù)據(jù)的API,如getData(int index),通過throws拋出一個(gè)異常,以表示API用戶所傳入的參數(shù)index是一個(gè)非法值。可以想象得到的是,由于getData()可能會(huì)被非常頻繁地使用,因此軟件開發(fā)人員需要在每一處調(diào)用都使用try … catch …塊來截獲該異常,從而使代碼顯得凌亂不堪。
如果一個(gè)類庫擁有一個(gè)這樣的API,那么該類庫中的這種對(duì)Checked Exception的不恰當(dāng)使用常常不止一個(gè)。那么該類庫的這些API會(huì)大量地污染用戶代碼,使得這些用戶代碼中充斥著不必要也沒有任何意義的try…catch…塊,進(jìn)而讓代碼邏輯顯得極為晦澀難懂。
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,而且軟件開發(fā)人員也能保證傳入的索引是合法的,那么代碼會(huì)簡(jiǎn)化很多:
1 Record record = library.getDataAt(2); 2 record.setIntValue(record.getIntValue() * 2); 3 library.setDataAt(2, record);
那么我們應(yīng)該在什么時(shí)候使用Checked Exception呢?就像前面所說的,如果一個(gè)異常所表示的并不是代碼本身的不足所導(dǎo)致的非正常狀態(tài),而是一系列應(yīng)用本身也無法控制的情況,那么我們將需要使用Checked Exception。就以前面所列出的FileReader類的構(gòu)造函數(shù)為例:
1 public FileReader(String fileName) throws FileNotFoundException
該構(gòu)造函數(shù)的簽名所表示的意義實(shí)際上是:
- 必須通過傳入的參數(shù)fileName來標(biāo)示需要打開的文件
- 如果文件存在,那么該構(gòu)造函數(shù)將返回一個(gè)FileReader類的實(shí)例
- 對(duì)該構(gòu)造函數(shù)進(jìn)行使用的代碼必須處理由fileName所標(biāo)示的文件不存在,進(jìn)而拋出FileNotFoundException的情況
也就是說,Checked Exception實(shí)際上是API設(shè)計(jì)中的一部分。在調(diào)用這個(gè)API的時(shí)候,你不得不處理目標(biāo)文件不存在的情況。而這則是由文件系統(tǒng)的自身特性所導(dǎo)致的。而之所以Checked Exception導(dǎo)致了如此多的爭(zhēng)論和誤用,更多是因?yàn)槲覀冊(cè)谟卯惓_@個(gè)用來表示應(yīng)用中的運(yùn)行錯(cuò)誤這個(gè)語言組成來通知用戶他所必須處理的應(yīng)用無法控制的可能情況。也就是說,其為異常賦予了新的含義,使得異常需要表示兩個(gè)完全不相干的概念。而在沒有仔細(xì)分辨的情況下,這兩個(gè)概念是極容易混淆的。因此在嘗試著定義一個(gè)Checked Exception之前,API編寫者首先要考慮這個(gè)異常所表示的到底是系統(tǒng)自身缺陷所導(dǎo)致的運(yùn)行錯(cuò)誤,還是要讓用戶自己來處理的邊緣情況。
正確地使用Checked Exception
實(shí)際上,如何正確地使用Checked Exception已經(jīng)在前面的各章節(jié)講解中進(jìn)行了詳細(xì)地說明。在這里我們?cè)俅巫鲆粋€(gè)總結(jié),同時(shí)也用來加深一下印象。
從API編寫者的角度來講,他所需要考慮的就是在何時(shí)使用一個(gè)Checked Exception。
首先,Checked Exception應(yīng)當(dāng)只在異常情況對(duì)于API以及API的使用者都無法避免的情況下被使用。例如在打開一個(gè)文件的時(shí)候,API以及API的使用者都沒有辦法保證該文件一定存在。反過來,在通過索引訪問數(shù)據(jù)的時(shí)候,如果API的使用者對(duì)參數(shù)index傳入的是-1,那么這就是一個(gè)代碼上的錯(cuò)誤,是完全可以避免的。因此對(duì)于index參數(shù)值不對(duì)的情況,我們應(yīng)該使用Unchecked Exception。
其次,Checked Exception不應(yīng)該被廣泛調(diào)用的API所拋出。這一方面是基于代碼整潔性的考慮,另一方面則是因?yàn)镃hecked Exception本身的實(shí)際意義是API以及API的使用者都無法避免的情況。如果一個(gè)應(yīng)用有太多處這種“無法避免的異常”,那么這個(gè)程序是否擁有足夠的質(zhì)量也是一個(gè)很值得考慮的問題。而就API提供者而言,在一個(gè)主要的被廣泛使用的功能上拋出這種異常,也是對(duì)其自身API的一種否定。
再次,一個(gè)Checked Exception應(yīng)該有明確的意義。這種明確意義的標(biāo)準(zhǔn)則是需要讓API使用者能夠看到這個(gè)Checked Exception所對(duì)應(yīng)的異常類,該異常類所包含的各個(gè)域,并閱讀相應(yīng)的API文檔以后就能夠了解到底哪里出現(xiàn)了問題,進(jìn)而向用戶提供準(zhǔn)確的有關(guān)該異常的解釋。
而對(duì)于API的用戶而言,一旦遇到了一個(gè)API會(huì)拋出Checked Exception,那么他就需要考慮使用一個(gè)Wrapped Exception來將該Checked Exception包裝起來。那什么是Wrapped Exception呢?
簡(jiǎn)單地說,Wrapped Exception就是將一個(gè)異常包裝起來的異常。在try…catch…塊捕獲到一個(gè)異常的時(shí)候,該異常內(nèi)部所記錄的消息可能并不合適。就以前面我們已經(jīng)舉過的加載偏好的示例為例。在啟動(dòng)時(shí),應(yīng)用會(huì)嘗試讀取用戶的偏好設(shè)置。這些偏好設(shè)置記錄在了一個(gè)文件中,卻可能已經(jīng)被誤刪除。在這種情況下,對(duì)該偏好文件的讀取會(huì)導(dǎo)致一個(gè)FileNotFoundException拋出。但是在該異常中所記錄的信息對(duì)于用戶,甚至應(yīng)用編寫者而言沒有任何價(jià)值:“Could not find file preference.xml while opening file”。在這種情況下,我們就需要構(gòu)造一個(gè)新的異常,在該異常中標(biāo)示準(zhǔn)確的錯(cuò)誤信息,并將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 }
上面的示例代碼中重新拋出了一個(gè)ApplicationSpecificException類型的異常。從它的名字就可以看出,其應(yīng)該是API使用者在應(yīng)用實(shí)現(xiàn)中所添加的應(yīng)用特有的異常。為了避免調(diào)用棧中的每一個(gè)函數(shù)都需要添加throws聲明,該異常需要從RuntimeException派生。這樣應(yīng)用就可以通過在調(diào)用棧的最底層捕捉這些異常并對(duì)這些異常進(jìn)行處理:在系統(tǒng)日志中添加一條異常記錄,只對(duì)用戶顯示異常中的消息,以防止異常內(nèi)部的調(diào)用棧信息暴露過多的實(shí)現(xiàn)細(xì)節(jié)等:
1 try { 2 …… 3 } catch(ApplicationSpecificException exception) { 4 logger.log(exception.getLevel(), exception.getMessage(), exception); 5 // 將exception內(nèi)部記錄的信息顯示給用戶(或添加到請(qǐng)求的響應(yīng)中返回) 6 // 如showWarningMessage(exception.getMessage()); 7 }
轉(zhuǎn)載請(qǐng)注明原文地址并標(biāo)明轉(zhuǎn)載:http://www.rzrgm.cn/loveis715/p/4596551.html
商業(yè)轉(zhuǎn)載請(qǐng)事先與我聯(lián)系:silverfox715@sina.com

浙公網(wǎng)安備 33010602011771號(hào)