如何優(yōu)雅的處理異常(轉(zhuǎn)載)

作者:ylxfc
鏈接:https://www.zhihu.com/question/28254987/answer/40173231
來源:知乎
著作權(quán)歸作者所有寄症。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。

Java中異常提供了一種識別及響應(yīng)錯(cuò)誤情況的一致性機(jī)制烘浦,有效地異常處理能使程序更加健壯保礼、易于調(diào)試揖盘。異常之所以是一種強(qiáng)大的調(diào)試手段,在于其回答了以下三個(gè)問題:

  • 什么出了錯(cuò)?
  • 在哪出的錯(cuò)?
  • 為什么出錯(cuò)?

在有效使用異常的情況下滑黔,異常類型回答了“什么”被拋出,異常堆棧跟蹤回答了“在哪“拋出环揽,異常信息回答了“為什么“會拋出略荡,如果你的異常沒有回答以上全部問題,那么可能你沒有很好地使用它們歉胶。有三個(gè)原則可以幫助你在調(diào)試過程中最大限度地使用好異常汛兜,這三個(gè)原則是:

  • 具體明確
  • 提早拋出
  • 延遲捕獲

為了闡述有效異常處理的這三個(gè)原則,本文通過杜撰個(gè)人財(cái)務(wù)管理器類JCheckbook進(jìn)行討論通今,JCheckbook用于記錄及追蹤諸如存取款,票據(jù)開具之類的銀行賬戶活動粥谬。
具體明確
Java定義了一個(gè)異常類的層次結(jié)構(gòu),其以Throwable開始,擴(kuò)展出Error和Exception辫塌,而Exception又?jǐn)U展出RuntimeException.如圖1所示.

圖1.Java異常層次結(jié)構(gòu)

這四個(gè)類是泛化的漏策,并不提供多少出錯(cuò)信息,雖然實(shí)例化這幾個(gè)類是語法上合法的(如:new Throwable())臼氨,但是最好還是把它們當(dāng)虛基類看掺喻,使用它們更加特化的子類。Java已經(jīng)提供了大量異常子類,如需更加具體巢寡,你也可以定義自己的異常類喉脖。

例 如:http://java.io package包中定義了Exception類的子類IOException,更加特化確的是 FileNotFoundException抑月,EOFException和ObjectStreamException這些IOException的子 類树叽。每一種都描述了一類特定的I/O錯(cuò)誤:分別是文件丟失,異常文件結(jié)尾和錯(cuò)誤的序列化對象流.異常越具體,我們的程序就能更好地回答”什么出了錯(cuò)”這個(gè) 問題谦絮。

捕 獲異常時(shí)盡量明確也很重要题诵。例如:JCheckbook可以通過重新詢問用戶文件名來處理FileNotFoundException,對于 EOFException层皱,它可以根據(jù)異常拋出前讀取的信息繼續(xù)運(yùn)行性锭。如果拋出的是ObjectStreamException,則程序應(yīng)該提示用戶文件 已損壞叫胖,應(yīng)當(dāng)使用備份文件或者其他文件草冈。

Java讓明確捕獲異常變得容易,因?yàn)槲覀兛梢詫ν籺ry塊定義多個(gè)catch塊,從而對每種異常分別進(jìn)行恰當(dāng)?shù)奶幚怼?/p>

File prefsFile = new File(prefsFilename);

try{
    readPreferences(prefsFile);
}
catch (FileNotFoundException e){
    // alert the user that the specified file
    // does not exist
}
catch (EOFException e){
    // alert the user that the end of the file
    // was reached
}
catch (ObjectStreamException e){
     // alert the user that the file is corrupted
}
catch (IOException e){
    // alert the user that some other I/O
    // error occurred
}

JCheckbook 通過使用多個(gè)catch塊來給用戶提供捕獲到異常的明確信息瓮增。舉例來說:如果捕獲了FileNotFoundException怎棱,它可以提示用戶指定另一 個(gè)文件,某些情況下多個(gè)catch塊帶來的額外編碼工作量可能是非必要的負(fù)擔(dān)绷跑,但在這個(gè)例子中拳恋,額外的代碼的確幫助程序提供了對用戶更友好的響應(yīng)。

除前三個(gè)catch塊處理的異常之外砸捏,最后一個(gè)catch塊在IOException拋出時(shí)給用戶提供了更泛化的錯(cuò)誤信息.這樣一來谬运,程序就可以盡可能提供具體的信息,但也有能力處理未預(yù)料到的其他異常垦藏。

有 時(shí)開發(fā)人員會捕獲范化異常梆暖,并顯示異常類名稱或者打印堆棧信息以求"具體"。千萬別這么干掂骏!用戶看到j(luò)ava.io.EOFException或者堆棧信息 只會頭疼而不是獲得幫助式廷。應(yīng)當(dāng)捕獲具體的異常并且用"人話"給用戶提示確切的信息。不過芭挽,異常堆棧倒是可以在你的日志文件里打印滑废。記住,異常和堆棧信息是用來幫助開發(fā)人 員而不是用戶的袜爪。

最后蠕趁,應(yīng)該注意到JCheckbook并沒有在readPreferences()中捕獲異常,而是將捕獲和處理異常留到用戶界面層來做辛馆,這樣就能用對話框或其他方式來通知用戶俺陋。這被稱為"延遲捕獲"豁延,下文就會談到。

提早拋出
異常堆棧信息提供了導(dǎo)致異常出現(xiàn)的方法調(diào)用鏈的精確順序腊状,包括每個(gè)方法調(diào)用的類名诱咏,方法名,代碼文件名甚至行數(shù)缴挖,以此來精確定位異常出現(xiàn)的現(xiàn)場袋狞。

java.lang.NullPointerException
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.<init>(FileInputStream.java:103)
at jcheckbook.JCheckbook.readPreferences(JCheckbook.java:225)
at jcheckbook.JCheckbook.startup(JCheckbook.java:116)
at jcheckbook.JCheckbook.<init>(JCheckbook.java:27)
at jcheckbook.JCheckbook.main(JCheckbook.java:318)

以 上展示了FileInputStream類的open()方法拋出NullPointerException的情況。不過注意 FileInputStream.close()是標(biāo)準(zhǔn)Java類庫的一部分映屋,很可能導(dǎo)致這個(gè)異常的問題原因在于我們的代碼本身而不是Java API苟鸯。所以問題很可能出現(xiàn)在前面的其中一個(gè)方法,幸好它也在堆棧信息中打印出來了棚点。

不幸的是早处,NullPointerException是Java中信息量最少的(卻也是最常遭遇且讓人崩潰的)異常。它壓根不提我們最關(guān)心的事情:到底哪里是null瘫析。所以我們不得不回退幾步去找哪里出了錯(cuò)砌梆。

通過逐步回退跟蹤堆棧信息并檢查代碼,我們可以確定錯(cuò)誤原因是向readPreferences()傳入了一個(gè)空文件名參數(shù)贬循。既然readPreferences()知道它不能處理空文件名咸包,所以馬上檢查該條件:

public void readPreferences(String filename)
throws IllegalArgumentException{
    if (filename == null){
         throw new IllegalArgumentException("filename is null");
    }  //if

   //...perform other operations...

   InputStream in = new FileInputStream(filename);

   //...read the preferences file...
}

通過提早拋出異常(又稱"迅速失敗")甘有,異常得以清晰又準(zhǔn)確。堆棧信息立即反映出什么出了錯(cuò)(提供了非法參數(shù)值)葡缰,為什么出錯(cuò)(文件名不能為空值)亏掀,以及哪里出的錯(cuò)(readPreferences()的前部分)。這樣我們的堆棧信息就能如實(shí)提供:

java.lang.IllegalArgumentException: filename is null
at jcheckbook.JCheckbook.readPreferences(JCheckbook.java:207)
at jcheckbook.JCheckbook.startup(JCheckbook.java:116)
at jcheckbook.JCheckbook.<init>(JCheckbook.java:27)
at jcheckbook.JCheckbook.main(JCheckbook.java:318)

另外泛释,其中包含的異常信息("文件名為空")通過明確回答什么為空這一問題使得異常提供的信息更加豐富滤愕,而這一答案是我們之前代碼中拋出的NullPointerException所無法提供的。

通過在檢測到錯(cuò)誤時(shí)立刻拋出異常來實(shí)現(xiàn)迅速失敗怜校,可以有效避免不必要的對象構(gòu)造或資源占用间影,比如文件或網(wǎng)絡(luò)連接。同樣茄茁,打開這些資源所帶來的清理操作也可以省卻魂贬。

延遲捕獲
菜鳥和高手都可能犯的一個(gè)錯(cuò)是,在程序有能力處理異常之前就捕獲它裙顽。Java編譯器通過要求檢查出的異常必須被捕獲或拋出而間接助長了這種行為付燥。自然而然的做法就是立即將代碼用try塊包裝起來,并使用catch捕獲異常愈犹,以免編譯器報(bào)錯(cuò)键科。

問 題在于,捕獲之后該拿異常怎么辦?最不該做的就是什么都不做勋颖∴挛耍空的catch塊等于把整個(gè)異常丟進(jìn)黑洞,能夠說明何時(shí)何處為何出錯(cuò)的所有信息都會永遠(yuǎn)丟失饭玲。把異常寫到日志中還稍微好點(diǎn)侥祭,至少還有記錄可查。但我們總不能指望用戶去閱讀或者理解日志文件和異常信息咱枉。讓readPreferences()顯示錯(cuò)誤信息對話框也不合適卑硫,因?yàn)殡m然JCheckbook目前是桌面應(yīng)用程序,但我們還計(jì)劃將它變成基于HTML的Web應(yīng)用蚕断。那樣的話欢伏,顯示錯(cuò)誤對話框顯然不是個(gè)選擇。同時(shí)亿乳,不管HTML還是C/S版本硝拧,配置信息都是在服務(wù)器上讀取的,而錯(cuò)誤信息需要顯示給Web瀏覽器或者客戶端程序葛假。 readPreferences()應(yīng)當(dāng)在設(shè)計(jì)時(shí)將這些未來需求也考慮在內(nèi)障陶。適當(dāng)分離用戶界面代碼和程序邏輯可以提高我們代碼的可重用性。

在有條件處理異常之前過早捕獲它聊训,通常會導(dǎo)致更嚴(yán)重的錯(cuò)誤和其他異常抱究。例如,如果上文的readPreferences()方法在調(diào)用FileInputStream構(gòu)造方法時(shí)立即捕獲和記錄可能拋出的FileNotFoundException带斑,代碼會變成下面這樣:

public void readPreferences(String filename){
   //...

   InputStream in = null;

   // DO NOT DO THIS!!!
try{
    in = new FileInputStream(filename);
}
catch (FileNotFoundException e){
    logger.log(e);
}

in.read(...);

//...
}

上 面的代碼在完全沒有能力從FileNotFoundException中恢復(fù)過來的情況下就捕獲了它鼓寺。如果文件無法找到,下面的方法顯然無法讀取它勋磕。如果 readPreferences()被要求讀取不存在的文件時(shí)會發(fā)生什么情況妈候?當(dāng)然,F(xiàn)ileNotFoundException會被記錄下來挂滓,如果我們 當(dāng)時(shí)去看日志文件的話苦银,就會知道。然而當(dāng)程序嘗試從文件中讀取數(shù)據(jù)時(shí)會發(fā)生什么赶站?既然文件不存在幔虏,變量in就是空的,一個(gè) NullPointerException就會被拋出贝椿。

調(diào)試程序時(shí)所计,本能告訴我們要看日志最后面的信息。那將會是NullPointerException团秽,非常讓人討厭的是這個(gè)異常非常不具體主胧。錯(cuò)誤信息不僅誤導(dǎo)我們什么出了錯(cuò)(真正的錯(cuò)誤是FileNotFoundException而不是NullPointerException)叭首,還誤導(dǎo)了錯(cuò)誤的出處。真正 的問題出在拋出NullPointerException處的數(shù)行之外踪栋,這之間有可能存在好幾次方法的調(diào)用和類的銷毀焙格。我們的注意力被這條小魚從真正的錯(cuò)誤處吸引了過來,一直到我們往回看日志才能發(fā)現(xiàn)問題的源頭夷都。

既然readPreferences() 真正應(yīng)該做的事情不是捕獲這些異常眷唉,那應(yīng)該是什么?看起來有點(diǎn)有悖常理囤官,通常最合適的做法其實(shí)是什么都不做冬阳,不要馬上捕獲異常。把責(zé)任交給 readPreferences()的調(diào)用者党饮,讓它來研究處理配置文件缺失的恰當(dāng)方法肝陪,它有可能會提示用戶指定其他文件,或者使用默認(rèn)值刑顺,實(shí)在不行的話也 許警告用戶并退出程序氯窍。

把異常處理的責(zé)任往調(diào)用鏈的上游傳遞的辦法,就是在方法的throws子句聲明異常蹲堂。在聲明可能拋出的異常時(shí)狼讨,注意越具體越好。這用于標(biāo)識出調(diào)用你方法的程序需要知曉并且準(zhǔn)備處理的異常類型柒竞。例如政供,“延遲捕獲”版本的readPreferences()可能是這樣的:

public void readPreferences(String filename)
throws IllegalArgumentException,
FileNotFoundException, IOException{
    if (filename == null){
           throw new IllegalArgumentException("filename is null");
     }  //if

     //...

     InputStream in = new FileInputStream(filename);

//...
}

技 術(shù)上來說,我們唯一需要聲明的異常是IOException朽基,但我們明確聲明了方法可能拋出FileNotFoundException布隔。 IllegalArgumentException不是必須聲明的,因?yàn)樗欠菣z查性異常(即RuntimeException的子類)踩晶。然而聲明它是為 了文檔化我們的代碼(這些異常也應(yīng)該在方法的JavaDocs中標(biāo)注出來)执泰。

當(dāng) 然枕磁,最終你的程序需要捕獲異常渡蜻,否則會意外終止。但這里的技巧是在合適的層面捕獲異常计济,以便你的程序要么可以從異常中有意義地恢復(fù)并繼續(xù)下去茸苇,而不導(dǎo)致更 深入的錯(cuò)誤;要么能夠?yàn)橛脩籼峁┟鞔_的信息沦寂,包括引導(dǎo)他們從錯(cuò)誤中恢復(fù)過來学密。如果你的方法無法勝任,那么就不要處理異常传藏,把它留到后面捕獲和在恰當(dāng)?shù)膶用嫣幚怼?br> 結(jié)論
經(jīng)驗(yàn)豐富的開發(fā)人員都知道腻暮,調(diào)試程序的最大難點(diǎn)不在于修復(fù)缺陷彤守,而在于從海量的代碼中找出缺陷的藏身之處。只要遵循本文的三個(gè)原則哭靖,就能讓你的異常協(xié)助你跟蹤和消滅缺陷具垫,使你的程序更加健壯,對用戶更加友好试幽。

英文原文:Jim Cushing筝蚕,編譯:ImportNew - 鄭瑋

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市铺坞,隨后出現(xiàn)的幾起案子起宽,更是在濱河造成了極大的恐慌,老刑警劉巖济榨,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坯沪,死亡現(xiàn)場離奇詭異,居然都是意外死亡腿短,警方通過查閱死者的電腦和手機(jī)屏箍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來橘忱,“玉大人赴魁,你說我怎么就攤上這事《鄢希” “怎么了颖御?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長凝颇。 經(jīng)常有香客問我潘拱,道長,這世上最難降的妖魔是什么拧略? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任芦岂,我火速辦了婚禮,結(jié)果婚禮上垫蛆,老公的妹妹穿的比我還像新娘禽最。我一直安慰自己,他們只是感情好袱饭,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布川无。 她就那樣靜靜地躺著,像睡著了一般虑乖。 火紅的嫁衣襯著肌膚如雪懦趋。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天疹味,我揣著相機(jī)與錄音仅叫,去河邊找鬼帜篇。 笑死,一個(gè)胖子當(dāng)著我的面吹牛诫咱,可吹牛的內(nèi)容都是我干的坠狡。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼遂跟,長吁一口氣:“原來是場噩夢啊……” “哼逃沿!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起幻锁,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤凯亮,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后哄尔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體假消,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年岭接,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了富拗。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,096評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鸣戴,死狀恐怖啃沪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情窄锅,我是刑警寧澤创千,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站入偷,受9級特大地震影響追驴,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜疏之,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一殿雪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧锋爪,春花似錦丙曙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽沃呢。三九已至年栓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間薄霜,已是汗流浹背某抓。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工纸兔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人否副。 一個(gè)月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓汉矿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親备禀。 傳聞我的和親對象是個(gè)殘疾皇子洲拇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Java中異常提供了一種識別及響應(yīng)錯(cuò)誤情況的一致性機(jī)制赋续,有效地異常處理能使程序更加健壯、易于調(diào)試另患。異常之所以是一種...
    路過的小哥哥閱讀 1,461評論 0 1
  • 六種異常處理的陋習(xí) 你覺得自己是一個(gè)Java專家嗎纽乱?是否肯定自己已經(jīng)全面掌握了Java的異常處理機(jī)制?在下面這段代...
    Executing閱讀 1,329評論 0 6
  • Java中的異常提供了一種識別和相應(yīng)錯(cuò)誤情況的一致性機(jī)制昆箕。有效處理異常能使得你的程序更加健壯和易于調(diào)試鸦列。異常之所以...
    驪驊閱讀 494評論 0 0
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)鹏倘,斷路器薯嗤,智...
    卡卡羅2017閱讀 134,659評論 18 139
  • 茫茫人海,大千世界纤泵,今日陌生環(huán)境应民,卻讓我有種家的感覺,感謝同學(xué)似家人的熱情款待夕吻,
    幸福鈴鐺閱讀 224評論 0 0