JAVA異常與異常處理詳解
一、異常簡(jiǎn)介
什么是異常?
異常就是有異于常態(tài),和正常情況不一樣,有錯(cuò)誤出錯(cuò)。在java中,阻止當(dāng)前方法或作用域的情況,稱之為異常。
java中異常的體系是怎么樣的呢?
1.Java中的所有不正常類都繼承于Throwable類。Throwable主要包括兩個(gè)大類,一個(gè)是Error類,另一個(gè)是Exception類;
二、try-catch-finally語(yǔ)句
(1)try塊:負(fù)責(zé)捕獲異常,一旦try中發(fā)現(xiàn)異常,程序的控制權(quán)將被移交給catch塊中的異常處理程序。
【try語(yǔ)句塊不可以獨(dú)立存在,必須與 catch 或者 finally 塊同存】
(2)catch塊:如何處理?比如發(fā)出警告:提示、檢查配置、網(wǎng)絡(luò)連接,記錄錯(cuò)誤等。執(zhí)行完catch塊之后程序跳出catch塊,繼續(xù)執(zhí)行后面的代碼。
【編寫catch塊的注意事項(xiàng):多個(gè)catch塊處理的異常類,要按照先catch子類后catch父類的處理方式,因?yàn)闀?huì)【就近處理】異常(由上自下)。】
(3)finally:最終執(zhí)行的代碼,用于關(guān)閉和釋放資源。
=======================================================================
語(yǔ)法格式如下:
當(dāng)異常出現(xiàn)時(shí),程序?qū)⒔K止執(zhí)行,交由異常處理程序(拋出提醒或記錄日志等),異常代碼塊外代碼正常執(zhí)行。 try會(huì)拋出很多種類型的異常,由多個(gè)catch塊捕獲多鐘錯(cuò)誤。
多重異常處理代碼塊順序問(wèn)題:先子類再父類(順序不對(duì)編譯器會(huì)提醒錯(cuò)誤),finally語(yǔ)句塊處理最終將要執(zhí)行的代碼。
=======================================================================
接下來(lái),我們用實(shí)例來(lái)鞏固try-catch語(yǔ)句吧~
先看例子:
結(jié)果分析:結(jié)果中的紅色字拋出的異常信息是由e.printStackTrace()來(lái)輸出的,它說(shuō)明了這里我們拋出的異常類型是算數(shù)異常,后面還跟著原因:by zero(由0造成的算數(shù)異常),下面兩行at表明了造成此異常的代碼具體位置。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
在上面例子中再加上一個(gè)test2()方法來(lái)測(cè)試finally語(yǔ)句的執(zhí)行狀況:
結(jié)果分析:我們可以從結(jié)果看出,finally語(yǔ)句塊是在try塊和catch塊語(yǔ)句執(zhí)行之后最后執(zhí)行的。finally是在return后面的表達(dá)式運(yùn)算后執(zhí)行的(此時(shí)并沒(méi)有返回運(yùn)算后的值,而是先把要返回的值保存起來(lái),管finally中的代碼怎么樣,返回的值都不會(huì)改變,仍然是之前保存的值),所以函數(shù)返回值是在finally執(zhí)行前確定的;
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
這里有個(gè)有趣的問(wèn)題,如果把上述中的test2方法中的finally語(yǔ)句塊中加上return,編譯器就會(huì)提示警告:finally block does not complete normally
分析問(wèn)題: finally塊中的return語(yǔ)句可能會(huì)覆蓋try塊、catch塊中的return語(yǔ)句;如果finally塊中包含了return語(yǔ)句,即使前面的catch塊重新拋出了異常,則調(diào)用該方法的語(yǔ)句也不會(huì)獲得catch塊重新拋出的異常,而是會(huì)得到finally塊的返回值,并且不會(huì)捕獲異常。
解決問(wèn)題:面對(duì)上述情況,其實(shí)更合理的做法是,既不在try block內(nèi)部中使用return語(yǔ)句,也不在finally內(nèi)部使用 return語(yǔ)句,而應(yīng)該在 finally 語(yǔ)句之后使用return來(lái)表示函數(shù)的結(jié)束和返回。如:
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
總結(jié):
1、不管有木有出現(xiàn)異常或者try和catch中有返回值return,finally塊中代碼都會(huì)執(zhí)行;
2、finally中最好不要包含return,否則程序會(huì)提前退出,返回會(huì)覆蓋try或catch中保存的返回值。
3. e.printStackTrace()可以輸出異常信息。
4. return值為-1為拋出異常的習(xí)慣寫法。
5. 如果方法中try,catch,finally中沒(méi)有返回語(yǔ)句,則會(huì)調(diào)用這三個(gè)語(yǔ)句塊之外的return結(jié)果。
6. finally 在try中的return之后 在返回主調(diào)函數(shù)之前執(zhí)行。
三、throw和throws關(guān)鍵字
java中的異常拋出通常使用throw和throws關(guān)鍵字來(lái)實(shí)現(xiàn)。
throw ----將產(chǎn)生的異常拋出,是拋出異常的一個(gè)動(dòng)作。
一般會(huì)用于程序出現(xiàn)某種邏輯時(shí)程序員主動(dòng)拋出某種特定類型的異常。如:
語(yǔ)法:throw (異常對(duì)象),如:
throws----聲明將要拋出何種類型的異常(聲明)。
語(yǔ)法格式:
throw與throws的比較
1、throws出現(xiàn)在方法函數(shù)頭;而throw出現(xiàn)在函數(shù)體。
2、throws表示出現(xiàn)異常的一種可能性,并不一定會(huì)發(fā)生這些異常;throw則是拋出了異常,執(zhí)行throw則一定拋出了某種異常對(duì)象。
3、兩者都是消極處理異常的方式(這里的消極并不是說(shuō)這種方式不好),只是拋出或者可能拋出異常,但是不會(huì)由函數(shù)去處理異常,真正的處理異常由函數(shù)的上層調(diào)用處理。
來(lái)看個(gè)例子:
throws e1,e2,e3只是告訴程序這個(gè)方法可能會(huì)拋出這些異常,方法的調(diào)用者可能要處理這些異常,而這些異常e1,e2,e3可能是該函數(shù)體產(chǎn)生的。
throw則是明確了這個(gè)地方要拋出這個(gè)異常。如:
分析:
1.代碼塊中可能會(huì)產(chǎn)生3個(gè)異常,(Exception1,Exception2,Exception3)。
2.如果產(chǎn)生Exception1異常,則捕獲之后再拋出,由該方法的調(diào)用者去處理。
3.如果產(chǎn)生Exception2異常,則該方法自己處理了(即System.out.println("出錯(cuò)了!");)。所以該方法就不會(huì)再向外拋出Exception2異常了,void doA() throws Exception1,Exception3 里面的Exception2也就不用寫了。因?yàn)橐呀?jīng)用try-catch語(yǔ)句捕獲并處理了。
4.Exception3異常是該方法的某段邏輯出錯(cuò),程序員自己做了處理,在該段邏輯錯(cuò)誤的情況下拋出異常Exception3,則該方法的調(diào)用者也要處理此異常。這里用到了自定義異常,該異常下面會(huì)由解釋。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
使用throw和throws關(guān)鍵字需要注意以下幾點(diǎn):
1.throws的異常列表可以是拋出一條異常,也可以是拋出多條異常,每個(gè)類型的異常中間用逗號(hào)隔開
2.方法體中調(diào)用會(huì)拋出異常的方法或者是先拋出一個(gè)異常:用throw new Exception() throw寫在方法體里,表示“拋出異常”這個(gè)動(dòng)作。
3.如果某個(gè)方法調(diào)用了拋出異常的方法,那么必須添加try catch語(yǔ)句去嘗試捕獲這種異常, 或者添加聲明,將異常拋出給更上一層的調(diào)用者進(jìn)行處理
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
自定義異常
為什么要使用自定義異常,有什么好處?
1.我們?cè)诠ぷ鞯臅r(shí)候,項(xiàng)目是分模塊或者分功能開發(fā)的 ,基本不會(huì)你一個(gè)人開發(fā)一整個(gè)項(xiàng)目,使用自定義異常類就統(tǒng)一了對(duì)外異常展示的方式。
2.有時(shí)候我們遇到某些校驗(yàn)或者問(wèn)題時(shí),需要直接結(jié)束掉當(dāng)前的請(qǐng)求,這時(shí)便可以通過(guò)拋出自定義異常來(lái)結(jié)束,如果你項(xiàng)目中使用了SpringMVC比較新的版本的話有控制器增強(qiáng),可以通過(guò)@ControllerAdvice注解寫一個(gè)控制器增強(qiáng)類來(lái)攔截自定義的異常并響應(yīng)給前端相應(yīng)的信息。
3.自定義異常可以在我們項(xiàng)目中某些特殊的業(yè)務(wù)邏輯時(shí)拋出異常,比如"中性".equals(sex),性別等于中性時(shí)我們要拋出異常,而Java是不會(huì)有這種異常的。系統(tǒng)中有些錯(cuò)誤是符合Java語(yǔ)法的,但不符合我們項(xiàng)目的業(yè)務(wù)邏輯。
4.使用自定義異常繼承相關(guān)的異常來(lái)拋出處理后的異常信息可以隱藏底層的異常,這樣更安全,異常信息也更加的直觀。自定義異常可以拋出我們自己想要拋出的信息,可以通過(guò)拋出的信息區(qū)分異常發(fā)生的位置,根據(jù)異常名我們就可以知道哪里有異常,根據(jù)異常提示信息進(jìn)行程序修改。比如空指針異常NullPointException,我們可以拋出信息為“xxx為空”定位異常位置,而不用輸出堆棧信息。
說(shuō)完了為什么要使用自定義異常,有什么好處,我們?cè)賮?lái)看看自定義異常的毛病:
毋庸置疑,我們不可能期待JVM(Java虛擬機(jī))自動(dòng)拋出一個(gè)自定義異常,也不能夠期待JVM會(huì)自動(dòng)處理一個(gè)自定義異常。發(fā)現(xiàn)異常、拋出異常以及處理異常的工作必須靠編程人員在代碼中利用異常處理機(jī)制自己完成。這樣就相應(yīng)的增加了一些開發(fā)成本和工作量,所以項(xiàng)目沒(méi)必要的話,也不一定非得要用上自定義異常,要能夠自己去權(quán)衡。
最后,我們來(lái)看看怎么使用自定義異常:
在 Java 中你可以自定義異常。編寫自己的異常類時(shí)需要記住下面的幾點(diǎn)。
所有異常都必須是 Throwable 的子類。
如果希望寫一個(gè)檢查性異常類,則需要繼承 Exception 類。
如果你想寫一個(gè)運(yùn)行時(shí)異常類,那么需要繼承 RuntimeException 類。
可以像下面這樣定義自己的異常類:
class MyException extends Exception{ }
我們來(lái)看一個(gè)實(shí)例:
就是這么簡(jiǎn)單,可以根據(jù)實(shí)際業(yè)務(wù)需求去拋出相應(yīng)的自定義異常。
四、java中的異常鏈
異常需要封裝,但是僅僅封裝還是不夠的,還需要傳遞異常。
異常鏈?zhǔn)且环N面向?qū)ο缶幊碳夹g(shù),指將捕獲的異常包裝進(jìn)一個(gè)新的異常中并重新拋出的異常處理方式。原異常被保存為新異常的一個(gè)屬性(比如cause)。這樣做的意義是一個(gè)方法應(yīng)該拋出定義在相同的抽象層次上的異常,但不會(huì)丟棄更低層次的信息。
我可以這樣理解異常鏈:
把捕獲的異常包裝成新的異常,在新異常里添加原始的異常,并將新異常拋出,它們就像是鏈?zhǔn)椒磻?yīng)一樣,一個(gè)導(dǎo)致(cause)另一個(gè)。這樣在最后的頂層拋出的異常信息就包括了最底層的異常信息。
》場(chǎng)景
比如我們的JEE項(xiàng)目一般都又三層:持久層、邏輯層、展現(xiàn)層,持久層負(fù)責(zé)與數(shù)據(jù)庫(kù)交互,邏輯層負(fù)責(zé)業(yè)務(wù)邏輯的實(shí)現(xiàn),展現(xiàn)層負(fù)責(zé)UI數(shù)據(jù)的處理。
有這樣一個(gè)模塊:用戶第一次訪問(wèn)的時(shí)候,需要持久層從user.xml中讀取數(shù)據(jù),如果該文件不存在則提示用戶創(chuàng)建之,那問(wèn)題就來(lái)了:如果我們直接把持久層的異常FileNotFoundException拋棄掉,邏輯層根本無(wú)從得知發(fā)生任何事情,也就不能為展現(xiàn)層提供一個(gè)友好的處理結(jié)果,最終倒霉的就是展現(xiàn)層:沒(méi)有辦法提供異常信息,只能告訴用戶“出錯(cuò)了,我也不知道出了什么錯(cuò)了”—毫無(wú)友好性而言。
正確的做法是先封裝,然后傳遞,過(guò)程如下:
1.把FileNotFoundException封裝為MyException。
2.拋出到邏輯層,邏輯層根據(jù)異常代碼(或者自定義的異常類型)確定后續(xù)處理邏輯,然后拋出到展現(xiàn)層。
3.展現(xiàn)層自行確定展現(xiàn)什么,如果管理員則可以展現(xiàn)低層級(jí)的異常,如果是普通用戶則展示封裝后的異常。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
》示例
結(jié)果分析:我們可以看到控制臺(tái)先是輸出了原始異常,這是由e.getCause()輸出的;然后輸出了e.printStackTrace(),在這里可以看到Caused by:原始異常和e.getCause()輸出的一致。這樣就是形成一個(gè)異常鏈。initCause()的作用是包裝原始的異常,當(dāng)想要知道底層發(fā)生了什么異常的時(shí)候調(diào)用getCause()就能獲得原始異常。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
》建議
異常需要封裝和傳遞,我們?cè)谶M(jìn)行系統(tǒng)開發(fā)的時(shí)候,不要“吞噬”異常,也不要“赤裸裸”的拋出異常,封裝后在拋出,或者通過(guò)異常鏈傳遞,可以達(dá)到系統(tǒng)更健壯、友好的目的。
五、結(jié)束語(yǔ)
java的異常處理的知識(shí)點(diǎn)雜而且理解起來(lái)也有點(diǎn)困難,我在這里給大家總結(jié)了以下幾點(diǎn)使用java異常處理的時(shí)候,良好的編碼習(xí)慣:
1、處理運(yùn)行時(shí)異常時(shí),采用邏輯去合理規(guī)避同時(shí)輔助try-catch處理
2、在多重catch塊后面,可以加一個(gè)catch(Exception)來(lái)處理可能會(huì)被遺漏的異常
3、對(duì)于不確定的代碼,也可以加上try-catch,處理潛在的異常
4、盡量去處理異常,切記只是簡(jiǎn)單的調(diào)用printStackTrace()去打印
5、具體如何處理異常,要根據(jù)不同的業(yè)務(wù)需求和異常類型去決定
6、盡量添加finally語(yǔ)句塊去釋放占用的資源
學(xué)習(xí)Java的同學(xué)注意了!!!
學(xué)習(xí)過(guò)程中遇到什么問(wèn)題或者想獲取學(xué)習(xí)資源的話,歡迎加入Java學(xué)習(xí)交流群346942462,我們一起學(xué)Java!