聊一聊Java 異常處理基礎(chǔ)篇
本文轉(zhuǎn)載自微信公眾號「蝸?;ヂ?lián)網(wǎng)」,作者白色蝸牛。轉(zhuǎn)載本文請聯(lián)系蝸牛互聯(lián)網(wǎng)公眾號。
閱讀本文你將收獲:
什么是異常
我們?nèi)粘I钪薪?jīng)常會(huì)遇到一些意外的事情,比如坐火車沒帶身份證,那你就無法順利上車。
計(jì)算機(jī)世界也有類似的情形,術(shù)語是異常(Exception),其實(shí)是異常事件(Exception Event)的縮寫。
一個(gè)異常就是一個(gè)事件,它發(fā)生在程序執(zhí)行過程中,會(huì)中斷程序的正常運(yùn)行。好比你上火車沒有身份證,這就是個(gè)異常事件,這個(gè)事件會(huì)阻擋你正常上火車。
計(jì)算機(jī)程序運(yùn)行會(huì)有個(gè)主入口,一般我們稱為 main 方法,main 方法內(nèi)部也可能調(diào)用各種其它方法。當(dāng)某個(gè)方法發(fā)生錯(cuò)誤時(shí),這個(gè)方法就會(huì)創(chuàng)建一個(gè)對象,并把它移交給運(yùn)行時(shí)的系統(tǒng)。這個(gè)對象就稱為異常對象,它包含了錯(cuò)誤相關(guān)的信息,包括錯(cuò)誤類型和程序狀態(tài)。
創(chuàng)建異常對象并將其交給運(yùn)行時(shí)系統(tǒng)這個(gè)操作就稱為拋出異常。
當(dāng)方法拋出異常后,運(yùn)行時(shí)系統(tǒng)會(huì)嘗試找到處理異常的方法。首先系統(tǒng)會(huì)判斷,錯(cuò)誤發(fā)生的方法有沒有處理,如果沒有,會(huì)把異常往上層方法拋,直到找到有異常處理的方法。這樣的話,從錯(cuò)誤發(fā)生的方法到異常處理的方法之間,就會(huì)形成調(diào)用方法的有序列表。
這個(gè)方法列表就稱為調(diào)用堆棧(call stack)。應(yīng)用程序的每個(gè)方法會(huì)按調(diào)用順序進(jìn)棧,棧是先進(jìn)后出的,比如 main 方法先進(jìn)棧,開始執(zhí)行程序,遇到其他方法的調(diào)用,其他方法也進(jìn)棧,其他方法執(zhí)行完畢,其他方法出棧,繼續(xù)執(zhí)行 main 方法,main 方法執(zhí)行完畢就出棧,???,程序運(yùn)行結(jié)束。
運(yùn)行時(shí)系統(tǒng)會(huì)在調(diào)用堆棧中尋找包含可以處理異常的代碼塊的方法,這段代碼就稱為異常處理程序。通過調(diào)用堆棧,從錯(cuò)誤發(fā)生的方法開始,按照方法調(diào)用相反的順序?qū)ふ?棧有先進(jìn)后出的特點(diǎn))。當(dāng)找到合適的異常處理程序時(shí),運(yùn)行時(shí)系統(tǒng)就會(huì)把異常傳遞給處理程序。如果拋出的異常對象的類型和處理程序可以處理的類型相匹配,就認(rèn)為異常處理程序是適當(dāng)?shù)摹?/p>
選中異常處理程序的過程就稱為捕獲異常。
如果運(yùn)行時(shí)系統(tǒng)找遍了調(diào)用堆棧上的所有方法,依然沒有找到適當(dāng)?shù)漠惓L幚沓绦?,那么運(yùn)行時(shí)系統(tǒng)(以及隨后的程序)將終止。
觀察以下代碼,想想運(yùn)行情況是怎樣的?
- package com.springtest.demo;
- public class Test {
- /**
- * 程序主方法
- *
- * @param args 程序入?yún)?nbsp;
- */
- public static void main(String[] args) {
- // 用戶輸入字符串 woniu
- String woniu = "woniu";
- int num = str2number(woniu);
- System.out.println(num);
- }
- /**
- * str 轉(zhuǎn) 整數(shù)
- *
- * @param str 字符串
- * @return 整數(shù)
- */
- private static int str2number(String str) {
- // 解析成數(shù)字,拋 NumberFormatException
- return Integer.parseInt(str);
- }
- }
輸出是這樣的:
- Exception in thread "main" java.lang.NumberFormatException: For input string: "woniu"
- at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
- at java.lang.Integer.parseInt(Integer.java:580)
- at java.lang.Integer.parseInt(Integer.java:615)
- at com.springtest.demo.Test.str2number(Test.java:29)
- at com.springtest.demo.Test.main(Test.java:15)
觀察運(yùn)行的結(jié)果信息,我們發(fā)現(xiàn)應(yīng)用主程序出現(xiàn)異常了,并且程序終止掉了,因?yàn)?num 的值并沒有打印。
結(jié)果里也告知我們是出現(xiàn)了 NumberFormatException,也就是數(shù)字格式異常,后邊也給到了提示,woniu 這個(gè)字符串是轉(zhuǎn)換不了數(shù)字的。這符合我們的預(yù)期。
然后就是調(diào)用堆棧,調(diào)用堆棧里的每一行信息都標(biāo)明了異常流轉(zhuǎn)過程中所在的方法路徑,類名以及代碼行數(shù)。
其中第一行信息就是異常最先發(fā)生的地方,這也可以作為我們異常排查的依據(jù)。
很明顯,在 forInputString 拋出異常后,parseInt 和 str2number 都只是轉(zhuǎn)發(fā)異常,并沒有捕獲異常,甚至在 main 方法中,也沒捕獲異常。最后因?yàn)闆]有異常處理程序,而導(dǎo)致程序運(yùn)行終止。
如何捕獲和處理異常
為了程序能夠正常運(yùn)行不被意外終止,Java 編程規(guī)范就有要求:異常必須要捕獲或者指定。
使用 try
捕獲異常的第一步是用 try 把可能引發(fā)異常的代碼括起來。
語法如下:
- try {
- // 可能引發(fā)異常的代碼
- }
try 包括了一個(gè)代碼塊,你可以把可能引發(fā)異常的代碼放里邊。代碼可以是一行,也可以是多行。這也意味著這個(gè)代碼塊可能引發(fā)多種不同的異常。
異常處理程序只有 try 是無法通過編譯的。你用 javac 命令編譯只有 try 的 java 文件,會(huì)報(bào)以下錯(cuò)誤:
錯(cuò)誤: 'try' 不帶有 'catch', 'finally' 或資源聲明
- 錯(cuò)誤: 'try' 不帶有 'catch', 'finally' 或資源聲明
- try {
- ^
- 1 個(gè)錯(cuò)誤
所以 try 代碼塊只是圈定了捕獲異常的范圍,只靠 try 做異常管理顯然不夠。
使用 catch
catch 語法
因此捕獲異常就需要第二步:用 catch 捕獲異常和異常處理。
語法如下:
- try {
- // 可能引發(fā)異常的代碼
- } catch (ExceptionType1 name1) {
- // 命中異常類型1 ExceptionType1 時(shí)的異常處理代碼
- } catch (ExceptionType2 name2) {
- // 命中異常類型2 ExceptionType2 時(shí)的異常處理代碼
- }
catch 是搭配 try 使用的,不單獨(dú)出現(xiàn)。try 后邊可以跟多個(gè) catch 代碼塊,以處理 try 中出現(xiàn)的多種類型的異常。
每個(gè) catch 代碼塊都是一個(gè)異常處理程序,處理的時(shí)候由 catch 的參數(shù)指定異常類型。
catch 的圓括號里,參數(shù) ExceptionType 聲明了這個(gè)處理程序可以處理的異常類型,這個(gè)異常類型必須是從 Throwable 類繼承的類。
Java 異常的繼承體系
提到 Throwable 就不得不說 Java 的異常體系。以下是 Java 異常的繼承體系圖。
Throwable 是異常體系的根,它繼承自 Object。Throwable 又拆分成兩個(gè)體系:Error 和 Exception。
Error 表示嚴(yán)重的錯(cuò)誤,程序一般無法處理,比如表示棧溢出的 StackOverflowError。
Exception 表示運(yùn)行時(shí)的錯(cuò)誤,它是可以被捕獲并處理的。Exception 又可以拆分為兩類:RuntimeException 和 Checked Exception。
RuntimeException 指運(yùn)行時(shí)異常,它是程序邏輯編寫不對造成的,比如表示空指針異常的 NullPointerException 以及表示數(shù)組索引越界的 IndexOutOfBoundsException。出現(xiàn)這種異常就是代碼 Bug,應(yīng)該修復(fù)程序代碼。
- int[] arrry = {0,1,2};
- // 此處會(huì)拋 java.lang.ArrayIndexOutOfBoundsException,不應(yīng)該出現(xiàn) arrry[3] 這樣的代碼
- System.out.println(arrry[3]);
Checked Exception 指檢測型異常,它是程序邏輯的一部分。比如表示 IO 異常的 IOException 以及表示文件找不到的 FileNotFoundException。這種異常必須捕獲并處理,否則編譯會(huì)失敗。
以下代碼是不能編譯通過的:
- public class A {
- public static void main(String[] args) {
- FileInputStream inputStream = new FileInputStream("/");
- }
- }
javac 編譯會(huì)報(bào)以下錯(cuò)誤,也會(huì)提示你必須用 try/catch 捕獲或者把異常添加到聲明里方便拋出。
- 錯(cuò)誤: 未報(bào)告的異常錯(cuò)誤FileNotFoundException; 必須對其進(jìn)行捕獲或聲明以便拋出
- FileInputStream inputStream = new FileInputStream("/");
- ^
- 1 個(gè)錯(cuò)誤
catch 使用
回到 catch 語法中,ExceptionType 對應(yīng)的就是 Java 異常體系中的 Exception 類或者它的子類。
name 是為異常類型起的名稱,花括號里的內(nèi)容就是調(diào)用異常處理程序時(shí)執(zhí)行的代碼,這里的代碼可以通過 name 這個(gè)名稱引用異常。
當(dāng)調(diào)用堆棧出現(xiàn)異常時(shí),運(yùn)行時(shí)系統(tǒng)會(huì)調(diào)用異常處理程序,當(dāng)異常處理程序的 ExceptionType 和引發(fā)異常的類型匹配時(shí),即命中某個(gè) catch 塊,就會(huì)把異常對象分配給異常處理程序的參數(shù),進(jìn)而執(zhí)行 catch 塊的異常處理代碼。
異常處理程序我們可以做很多事情,比如打印錯(cuò)誤日志,暫停程序,執(zhí)行錯(cuò)誤恢復(fù),也可以提示給用戶,或者把異常往上層傳遞。以下是打印錯(cuò)誤信息的示例代碼:
- public static void main(String[] args) {
- try {
- int[] arrry = {0, 1, 2};
- // 此處會(huì)拋 java.lang.ArrayIndexOutOfBoundsException,不應(yīng)該出現(xiàn) arrry[3] 這樣的代碼
- System.out.println(arrry[3]);
- } catch (IndexOutOfBoundsException e) {
- System.out.println("捕獲到數(shù)組越界異常:");
- System.out.println(e);
- }
- }
輸出結(jié)果為:
- 捕獲到數(shù)組越界異常:
- java.lang.ArrayIndexOutOfBoundsException: 3
有些場景,我們的一段代碼可能引發(fā)多種異常,而異常的處理會(huì)比較一致,比如都是打印日志,這種情況下,如果都單獨(dú)設(shè)置一個(gè) catch 塊,寫相同的代碼,重復(fù)度就很高。
因此在 Java 7 之后,一個(gè) catch 塊就支持處理多種類型的異常。語法如下:
- try {
- // 可能引發(fā)異常的代碼
- } catch (ExceptionType1 name1 | ExceptionType2 name2) {
- // 命中異常類型1 ExceptionType1 或異常類型2 ExceptionType2 時(shí)的異常處理代碼
- }
使用 finally
程序在運(yùn)行的時(shí)候有時(shí)候會(huì)打開一些資源,比如文件,連接,線程等等。如果程序運(yùn)行中途拋異常,程序終止,打開的資源就永遠(yuǎn)得不到釋放了,這會(huì)導(dǎo)致資源泄漏,甚至系統(tǒng)崩潰。
再比如,程序運(yùn)行結(jié)束前,我要輸出一個(gè)摘要日志做監(jiān)控,但如果運(yùn)行中途拋異常,程序終止,日志就不會(huì)打印,我也看不到我想要的信息。
因此需要有種機(jī)制,能夠支持在異常發(fā)生,阻斷流程的時(shí)候,也能把打開的資源釋放掉或者執(zhí)行指定的邏輯。
Java 用 finally 來達(dá)成這種目的,finally 可以形成try-finally 結(jié)構(gòu),也可以形成 try-catch-finally 結(jié)構(gòu)。但是 finally 代碼塊總是在 try 退出時(shí)執(zhí)行。
這個(gè)「總是」可以分為以下幾種情況:
無異常
try 執(zhí)行完畢,未發(fā)生異常,然后執(zhí)行 finally 代碼塊,像普通程序一樣順序執(zhí)行。
- public static void main(String[] args) {
- System.out.println("main:" + fetchMyName());
- }
- public static String fetchMyName() {
- String me = "woniu";
- try {
- me = "woniu666";
- } finally {
- System.out.println("finally: " + me);
- }
- return me;
- }
輸出:
- finally: woniu666
- main:woniu666
有異常未捕獲
try 執(zhí)行過程中出現(xiàn)異常,會(huì)把異常對象拋出,但 finally 代碼塊依然會(huì)執(zhí)行。
- public static void main(String[] args) {
- System.out.println("main:" + fetchMyName());
- }
- public static String fetchMyName() {
- String me = "woniu";
- int[] arrry = {0, 1, 2};
- try {
- me = "woniu666";
- // 此處會(huì)拋 java.lang.ArrayIndexOutOfBoundsException,不應(yīng)該出現(xiàn) arrry[3] 這樣的代碼
- System.out.println(arrry[3]);
- } finally {
- System.out.println("finally: " + me);
- }
- return me;
- }
fetchMyName() 未捕獲到異常,就往上拋,但會(huì)把 finally 里的邏輯先執(zhí)行掉,在 main 方法中同樣沒有捕獲異常,于是就阻斷了程序,打印出了調(diào)用堆棧。
- finally: woniu666
- Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
- at com.springtest.demo.TryFinally.fetchMyName(TryFinally.java:28)
- at com.springtest.demo.TryFinally.main(TryFinally.java:15)
有異常有捕獲
try 執(zhí)行過程中出現(xiàn)異常,會(huì)把異常對象拋出,catch 捕獲異常并正常處理,此時(shí) finally 代碼塊依然會(huì)執(zhí)行。
- public static void main(String[] args) {
- System.out.println("main:" + fetchMyName());
- }
- public static String fetchMyName() {
- String me = "woniu";
- int[] arrry = {0, 1, 2};
- try {
- me = "woniu666";
- // 此處會(huì)拋 java.lang.ArrayIndexOutOfBoundsException,不應(yīng)該出現(xiàn) arrry[3] 這樣的代碼
- System.out.println(arrry[3]);
- } catch (ArrayIndexOutOfBoundsException e) {
- System.out.println("命中數(shù)組索引越界異常的處理器,越界索引為:" + e.getMessage());
- } finally {
- System.out.println("finally: " + me);
- }
- return me;
- }
代碼正常運(yùn)行,先執(zhí)行了 catch 代碼塊中的邏輯,然后執(zhí)行了 finally 代碼塊,最后執(zhí)行 main 方法。
- 命中數(shù)組索引越界異常的處理器,越界索引為:3
- finally: woniu666
- main:woniu666
try 中 return
return 意味著方法執(zhí)行結(jié)束,而 finally 是在 try 退出時(shí)執(zhí)行,那如果 try 代碼塊中帶 return,finally 代碼塊還會(huì)執(zhí)行到么?
try 代碼塊加個(gè) return 試試!
- public static void main(String[] args) {
- System.out.println("main:" + fetchMyName());
- }
- public static String fetchMyName() {
- String me = "woniu";
- int[] arrry = {0, 1, 2};
- try {
- me = "woniu666";
- // 此處不拋異常
- System.out.println(arrry[0]);
- return "try";
- } catch (ArrayIndexOutOfBoundsException e) {
- System.out.println("命中數(shù)組索引越界異常的處理器,越界索引為:" + e.getMessage());
- } finally {
- System.out.println("finally: " + me);
- }
- return me;
- }
看結(jié)果依然會(huì)走到 finally 代碼塊的執(zhí)行!
- 0
- finally: woniu666
- main:try
catch 中 return
try 中 return 我們試了,那 catch 中 return,finally 的執(zhí)行是啥樣的呢?
- public static void main(String[] args) {
- System.out.println("main:" + fetchMyName());
- }
- public static String fetchMyName() {
- String me = "woniu";
- int[] arrry = {0, 1, 2};
- try {
- me = "woniu666";
- // 此處會(huì)拋 java.lang.ArrayIndexOutOfBoundsException,不應(yīng)該出現(xiàn) arrry[3] 這樣的代碼
- System.out.println(arrry[3]);
- } catch (ArrayIndexOutOfBoundsException e) {
- System.out.println("命中數(shù)組索引越界異常的處理器,越界索引為:" + e.getMessage());
- return "catch";
- } finally {
- System.out.println("finally: " + me);
- }
- return me;
- }
看結(jié)果依然會(huì)走到 finally 代碼塊的執(zhí)行!
- 命中數(shù)組索引越界異常的處理器,越界索引為:3
- finally: woniu666
- main:catch
如何指定方法拋出的異常
異常捕獲的知識(shí)介紹完之后,你想象另外一種情況,就是當(dāng)前方法拋出異常后,但是呢,當(dāng)前方法不適合處理這個(gè)異常,而調(diào)用堆棧上層的方法更適合處理。那其實(shí)當(dāng)前方法最好就不要捕獲異常,并能夠允許調(diào)用堆棧上層的方法處理它。
此時(shí),如果拋出的異常是 檢查型異常,那你就必須在方法上指定它可以拋出這些異常。你需要在方法聲明中添加一個(gè) throws 語句。throws 語句包含 throws 關(guān)鍵字,后面跟著由該方法一引發(fā)的所有異常,多個(gè)異常用逗號分隔。throws 語句放在方法名和參數(shù)列表之后,放在定義方法范圍的圓括號之前。
代碼示例如下:
- public static void test() throws FileNotFoundException {
- FileInputStream inputStream = new FileInputStream("/");
- }
由上層 main 方法捕獲處理:
- public static void main(String[] args) {
- try {
- test();
- } catch (FileNotFoundException e) {
- System.out.println("文件找不到異常:" + e.getMessage());
- }
- }
可以正常輸出:
- 文件找不到異常:/ (Is a directory)
前邊說檢查型異常必須要處理,是因?yàn)椴惶幚頃?huì)編譯不通過,要么捕獲和處理異常,要么指定方法拋出的異常,
那非檢查型異常,也就是運(yùn)行時(shí)異常也有這種要求么?
非檢查型異常并不強(qiáng)制,你可以指定方法拋出的異常,也可以不指定,不指定的時(shí)候,異常對象會(huì)不停的沿著調(diào)用堆棧向上層拋,直到被捕獲處理或者程序終止。
小結(jié)
本文介紹了異常的概念,我們了解到了異常相關(guān)的術(shù)語,異常出現(xiàn)的背景以及異常的運(yùn)行機(jī)制,接著我們按照 Java 編程規(guī)范分別介紹了異常如何捕獲以及異常如何指定,同時(shí)也介紹了 Java 異常的繼承體系。這些都是非常基礎(chǔ)的內(nèi)容,卻也非常重要,寫代碼的時(shí)候必須要考慮這方面,甚至你可以認(rèn)為,面向異常編程非常考驗(yàn)?zāi)愕木幋a功底。