Java異常處理
簡(jiǎn)介:異常處理是java語(yǔ)言的重要特性之一,《Three Rules for effective Exception Handling》一文中是這么解釋的:它主要幫助我們?cè)赿ebug的過(guò)程中解決下面的三個(gè)問(wèn)題。
什么出錯(cuò)了
哪里出錯(cuò)了
為什么出錯(cuò)
java語(yǔ)言可以說(shuō)是提供了過(guò)于完善的異常處理機(jī)制,以致于后來(lái)《Thinking in java》的作者Bruce Eckel都專門(mén)對(duì)他進(jìn)行了論述。java中的異常機(jī)制包括Error和Exception兩個(gè)部分。他們都繼承自一個(gè)共同的基類Throwable。Error屬于JVM運(yùn)行中發(fā)生的一些錯(cuò)誤,雖然并不屬于開(kāi)發(fā)人員的范疇,但是有些Error還是由代碼引起的,比如StackOverflowError經(jīng)常由遞歸操作引起,這種錯(cuò)誤就是告訴開(kāi)發(fā)者,你一般無(wú)法挽救,只能靠JVM。而Exception假設(shè)程序員會(huì)去處理這些異常,比如數(shù)據(jù)庫(kù)連接出了異常,那么我們可以處理這個(gè)異常,并且重新連接等。Exception分為兩種,檢查類型(checked)和未檢查類型(unchecked)。檢查類型的異常就是說(shuō)要程序員明確的去聲明或者用try..catch語(yǔ)句來(lái)處理的異常,而非檢查類型的異常則沒(méi)有這些限制,比如我們常見(jiàn)的 NullPointerException 就是非檢查類型的,他繼承自RuntimeException。java是目前主流編程語(yǔ)言中唯一一個(gè)推崇使用檢查類型異常的,至少sun是這樣的。關(guān)于使用checked還是unchecked異常的論戰(zhàn)一直很激烈。下面是一張java語(yǔ)言中異常的類關(guān)系圖。
基本使用
我們?cè)谑褂胘ava的一些文件或者數(shù)據(jù)庫(kù)操作的時(shí)候已經(jīng)接觸過(guò)一些異常了,比如IOException、SQLException等,這些方法被聲明可能會(huì)拋出某種異常,因此我們需要對(duì)其進(jìn)行捕獲處理。這就需要基本的try..catch語(yǔ)句了。下圖就是我們經(jīng)常寫(xiě)的一個(gè)基本結(jié)構(gòu)。try語(yǔ)句塊中寫(xiě)可能會(huì)拋出異常的代碼,之后在catch語(yǔ)句塊中進(jìn)行捕獲。我們看到catch的參數(shù)寫(xiě)的是一個(gè)Exception對(duì)象,這就意味著這個(gè)語(yǔ)句塊可以捕獲所有的檢查類型的異常(雖然這并不是一種好的寫(xiě)法,稍后討論),finally總是會(huì)保證在***執(zhí)行,一般我們?cè)诶锩嫣幚硪恍┣謇淼墓ぷ?,比如關(guān)閉文件流或者數(shù)據(jù)庫(kù),網(wǎng)絡(luò)等操作。
當(dāng)然上面的語(yǔ)句塊結(jié)構(gòu)是靈活的,但是try是必須有的,catch和finally兩者至少有一個(gè),當(dāng)然catche的數(shù)量可以有多個(gè)。有時(shí)候try語(yǔ)句塊中可能拋出多種類型的異常,這個(gè)時(shí)候,我們可以寫(xiě)多個(gè)catch語(yǔ)句來(lái)捕獲不同類型的異常,一個(gè)比較好的寫(xiě)法如下:
- try{
- // ..invoke some methods that may throw exceptions
- }catch(ExceptionType1 e){
- //...handle exception
- }catch(ExceptionType2 e){
- //...handle exception
- }catch(Exception e){
- //...handle exception
- }finally{
- //..do some cleaning :close the file db etc.
- }
當(dāng)異常不滿足前兩個(gè)type的時(shí)候,exception會(huì)將異常捕獲。我們發(fā)現(xiàn)這個(gè)寫(xiě)法比較類似switch case的結(jié)構(gòu)控制語(yǔ)句,但實(shí)際上,一旦某個(gè)catch得到匹配后,其他的就不會(huì)就匹配了,有點(diǎn)像加了break的case。有一點(diǎn)需要注意catch(Exception)一定要寫(xiě)在***面,catch是順序匹配的,后面匹配Exception的子類,編譯器就會(huì)報(bào)錯(cuò)。
初次學(xué)習(xí)try..catch總會(huì)被其吸引,所以大量的使用這種結(jié)果,以達(dá)到某種“魯棒性”。(這語(yǔ)句也是程序員表白的***)。但try語(yǔ)句實(shí)際上執(zhí)行的時(shí)候會(huì)導(dǎo)致棧操作。即要保存整個(gè)方法的調(diào)用路徑,這勢(shì)必會(huì)使得程序變慢。fillInStackTrace()是Throwable的一個(gè)方法,用來(lái)執(zhí)行棧的操作,他是線程同步的,本身也很耗時(shí)。這里問(wèn)題在StackOverFlow上曾經(jīng)有過(guò)一段非常經(jīng)典的討論,原文。 的確當(dāng)我們?cè)趖ry中什么都不做,或者只執(zhí)行一個(gè)類似加法的簡(jiǎn)單調(diào)用,那么其執(zhí)行效率和goto這樣的控制語(yǔ)句是幾乎一樣的。但是誰(shuí)會(huì)寫(xiě)這樣的代碼呢?
總之不要總是試圖通過(guò)try catch來(lái)控制程序的結(jié)構(gòu),無(wú)論從效率還是代碼的可讀性上都不好。
try catch好的一面
try catch雖然不推薦用于程序結(jié)構(gòu)的控制,但是也具有重要的意義,其設(shè)計(jì)的一個(gè)好處就是,開(kāi)發(fā)人員可以把一件事情當(dāng)做事務(wù)來(lái)處理,事務(wù)也是數(shù)據(jù)庫(kù)中重要的概念,舉個(gè)例子,比如完成訂單的這個(gè)事務(wù),其中包括了一個(gè)動(dòng)作序列,包括用戶提交訂單,商品出庫(kù),關(guān)聯(lián)等。當(dāng)這個(gè)序列中某一個(gè)動(dòng)作執(zhí)行失敗的時(shí)候,數(shù)據(jù)統(tǒng)一恢復(fù)到一個(gè)正常的點(diǎn),這樣就不會(huì)出現(xiàn),你付完了帳,商品卻沒(méi)有給你的情況。我們?cè)趖ry語(yǔ)句塊中就像執(zhí)行一個(gè)事務(wù)一樣,當(dāng)出現(xiàn)了異常,就會(huì)在catch中得到統(tǒng)一的處理,保證數(shù)據(jù)的完整無(wú)損。其實(shí)很多不好的代碼也是因?yàn)闆](méi)有好好利用catch語(yǔ)句的語(yǔ)言,導(dǎo)致很多異常就被淹沒(méi)了,這個(gè)后面介紹。
定制詳細(xì)的異常
我們可以自己定義異常,以捕獲處理某個(gè)具體的例子。創(chuàng)建自己的異常類,可以直接繼承Exception或者RuntimeException。區(qū)別是前者是簡(jiǎn)稱類型的,而后者為檢查類型異常。Sun官方力挺傳統(tǒng)的觀點(diǎn),他建議開(kāi)發(fā)者都是用檢查類型的異常,即你一定要去處理的異常。下面是定義的一個(gè)簡(jiǎn)單的異常類.
- public class SimpleException extends Exception{
- SimpleException(){}
- SimpleException(String info){
- super(info);
- }
- }
我們覆寫(xiě)了兩個(gè)構(gòu)造方法,這是有意義的。通過(guò)傳遞字符串參數(shù),我們創(chuàng)建一個(gè)異常對(duì)象的時(shí)候,可以記錄下詳細(xì)的信息,這樣這個(gè)異常被捕獲的時(shí)候就會(huì)顯示我們之前定義的詳細(xì)信息。比如用下面的代碼測(cè)試一下我們定義的異常類:
- public class Test {
- public void fun() throws SimpleException{
- throw new SimpleException("throwing from fun");
- }
- public static void main(String[] args) {
- Test t = new Test();
- try{
- t.fun();
- }catch(SimpleException e){
- e.printStackTrace();
- }
- }
- }
運(yùn)行就會(huì)得到下面的結(jié)果 printStackTrace是打印調(diào)用棧的方法,他有三個(gè)重載方法,默認(rèn)的是將信息輸出到System.err。這樣我們就可以清晰的看到方法調(diào)用的過(guò)程,有點(diǎn)像操作系統(tǒng)中的中斷,保護(hù)現(xiàn)場(chǎng)。
SimpleException: throwing from fun at Test.fun(Test.java:4) at Test.main(Test.java:9) |
略微麻煩的語(yǔ)法
我們自己實(shí)現(xiàn)的異常有時(shí)候會(huì)用到繼承這些特性,在異常繼承的時(shí)候有一些限制。那就是子類不能拋出基類或所實(shí)現(xiàn)的接口中沒(méi)有拋出的異常.比如有如下的接口:
- public interface InterfaceA {
- public void f() throws IOException;
- }
我們的Test類實(shí)現(xiàn)這個(gè)接口,那么Test的f方法要么不拋出異常,要么只能拋出IOException,其實(shí)關(guān)于這里還有更瑣碎的規(guī)矩,詳細(xì)可以參考《Java Puzzlers》第37個(gè)謎題。所以這和傳統(tǒng)的繼承和實(shí)現(xiàn)接口正好相反,面向?qū)ο蟮睦^承是擴(kuò)大化,而這正好是縮小了。
關(guān)于checked和unchecked的論戰(zhàn)
傳統(tǒng)的觀點(diǎn)里,sun認(rèn)為"因?yàn)?Java 語(yǔ)言并不要求方法捕獲或者指定運(yùn)行時(shí)異常,因此編寫(xiě)只拋出運(yùn)行時(shí)異常的代碼或者使得他們的所有異常子類都繼承自 RuntimeException ,對(duì)于程序員來(lái)說(shuō)是有吸引力的。這些編程捷徑都允許程序員編寫(xiě) Java 代碼而不會(huì)受到來(lái)自編譯器的所有挑剔性錯(cuò)誤的干擾,并且不用去指定或者捕獲任何異常。 盡管對(duì)于程序員來(lái)說(shuō)這似乎比較方便,但是它回避了 Java 的捕獲或者指定要求的意圖,并且對(duì)于那些使用您提供的類的程序員可能會(huì)導(dǎo)致問(wèn)題。"他強(qiáng)調(diào)盡量不使用unchecked異常。
但《Thinking in java》的作者Eckel卻改變了自己的想法, 他在自己博客上的一篇文章(這篇文章很好,表達(dá)也很簡(jiǎn)單)專門(mén)列舉了使用checked異常的弊端。他指出正式檢查類型讓導(dǎo)致了很多的異常不能被程序員發(fā)現(xiàn)。開(kāi)發(fā)人員有更大的自由去決定是不是要處理一個(gè)異常。即使忘記處理了某個(gè)異常,他也會(huì)在某個(gè)地方拋出來(lái)被發(fā)現(xiàn),而不至于丟失。checked異常使得代碼的可讀性變差,并且正在暗暗的鼓勵(lì)人們?nèi)パ蜎](méi)異常?,F(xiàn)在很多IDE都在提醒我們,某個(gè)方法要跑出異常,然后甚至自動(dòng)幫我們生成catch或者throw。這是非??膳碌男袨?,這導(dǎo)致了我們很多catch語(yǔ)句里面什么都沒(méi)有,就像一個(gè)陷阱一樣。
checked異常帶來(lái)的另一個(gè)問(wèn)題是,代碼的難維護(hù)性,因?yàn)橐诜椒暶魃霞由蟭hrows,如果方法的實(shí)現(xiàn)發(fā)生了某個(gè)變化,有了新的異常,那么我們不得不去修改方法的聲明。還有一點(diǎn)不好的就是不能明確的暴露異常的特征。比如我們登錄成績(jī)系統(tǒng)的時(shí)候,如果用戶名注冊(cè),我們可能期待一個(gè)NoSuchStudentException但是實(shí)際看到的可能是一個(gè)SQLException?!禘ffective java》中第 43 條:拋出與抽象相適應(yīng)的異常。講的就是這個(gè)原則,即拋出的異常應(yīng)該是和抽象的概念一致的,比如我們?cè)谝粋€(gè)系統(tǒng)無(wú)論遇到什么具體的問(wèn)題,但是大部分我們看到的都只是SQLException而已。
關(guān)于如何選擇,Bloch的建議是為可恢復(fù)的條件使用檢查型異常,為編程錯(cuò)誤使用運(yùn)行時(shí)異常。我的感覺(jué)是選擇檢查的異常就一定要”處理“,當(dāng)然此處的處理一定是真正的處理而不是空寫(xiě)一個(gè)catch語(yǔ)句而已。不知道未來(lái)的java會(huì)怎樣對(duì)待checked和unchecked,畢竟現(xiàn)在java是唯一一個(gè)支持檢查異常的主流編程語(yǔ)言了。
好的原則
Fail Fast:就是要盡早的拋出異常,這樣有有助于更加精確的定位出錯(cuò)的地點(diǎn)和原因。這個(gè)也比較好理解,比如用戶名字不合法的時(shí)候馬上拋出,UserNameIllegalException,如果沒(méi)有及時(shí)拋出異常,那么不合法的名字可能會(huì)導(dǎo)致一個(gè)SQLException,但是程序報(bào)給你一個(gè)SQLException,你卻很難直接得知一定是用戶名不合法造成的。Fail Fast這種思想,在java實(shí)現(xiàn)ArrayList的機(jī)制中也有很好的體現(xiàn)。
Catch late:不要在方法內(nèi)部過(guò)早的處理異常,特別是什么也不做的處理,那就更加的可怕了。因?yàn)槿绻?ldquo;無(wú)作為”的處理很可能導(dǎo)致后面繼續(xù)出現(xiàn)新的異常(比如錯(cuò)誤的用戶名會(huì)引發(fā)后面一些列錯(cuò)誤,程序還不能處理好錯(cuò)誤的用戶名,后面的就更處理不了了),這就給調(diào)試增加了很大的困難。一個(gè)好的經(jīng)驗(yàn)是將異常處理交給調(diào)用者,方法只在及時(shí)的地方拋出異常,技術(shù)上實(shí)現(xiàn)的方式就是給方法聲明throws,標(biāo)出所有可能要拋出的異常。
Doc:文檔的重要性,特別是非檢查的異常,一定要在文檔中注明。
異常處理是java非常重要的特性,上面是一些關(guān)于異常使用的討論,當(dāng)然更多知識(shí)還是需要實(shí)踐中發(fā)現(xiàn)。
原文鏈接:http://www.cnblogs.com/octobershiner/archive/2012/12/20/2827120.html
【編輯推薦】