前些章節(jié)的知識點(diǎn)有時會涉及到異常的知識,如果沒有專門學(xué)習(xí)過異常的小伙伴可能看的有點(diǎn)疑惑。今天這節(jié)就是為了講解異常,讓我們來了解什么是異常,它的作用是啥,怎么使用異常。
1. 異常的背景
1.1 邂逅異常
大家在學(xué)習(xí) Java
時,應(yīng)該也遇見過一些異常了,例如
算術(shù)異常:
System.out.println(10 / 0);
結(jié)果為:Exception in thread "main" java.lang.ArithmeticException: / by zero
數(shù)組越界異常:
int[] arr = {1, 2, 3, 4, 5}; System.out.println(arr[100]);
結(jié)果為:Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 100
空指針異常:
int[] arr = null; System.out.println(arr.length);
結(jié)果為:Exception in thread "main" java.lang.NullPointerException
那么什么是異常呢?
異常是程序中的一些錯誤,但并不是所有的錯誤都是異常,并且錯誤有時候是可以避免的。
1.2 異常和錯誤
- 異常被分為下面兩種
運(yùn)行時異常(非受查異常):
在程序運(yùn)行(通過編譯已經(jīng)得到了字節(jié)碼文件,再由 JVM 執(zhí)行)的過程當(dāng)中發(fā)生的異常,是可能被大家避免的異常,這些異常在編譯時可以被忽略。
例如:算數(shù)異常、空指針異常、數(shù)組越界異常等等
編譯時異常(受查異常):
編譯時發(fā)生的異常,這個異常是大家難以預(yù)見的,這些異常在編譯時不能被簡單的忽略。
例如:要打開一個不存在的文件時,一個異常就發(fā)生了
除了異常我們我們也要了解下錯誤
錯誤:
錯誤不是異常,而是脫離程序員控制的問題,錯誤在代碼中通常被忽略。
例如:當(dāng)棧溢出時,一個錯誤就發(fā)生了,這是編譯檢查不到的
public static void func(){ func(); } public static void main(String[] args){ func(); }結(jié)果為:
Exception in thread "main" java.lang.StackOverflowError
那么異常和錯誤的區(qū)別是什么呢?
出現(xiàn)錯誤必須由我們程序員去處理它的邏輯錯誤,而出現(xiàn)異常我們只要去處理異常就好了
如果有疑惑的伙伴通過后面的介紹你會逐漸了解它們的區(qū)別
1.3 Java 異常的體系(含體系圖)
Java
中異常的種類是很多的,我將一些異常收集并歸類如下
其中
Error
是錯誤,Exception
是異常。而異常中又分為了兩種,黃色的是編譯時異常,橙色的是運(yùn)行時異常。
但是這張圖不僅僅說明了上述的關(guān)系,我們還要知道
每個異常其實(shí)都是一個類,并且箭頭代表了繼承的關(guān)系
我們可以通過一個代碼來理解
int[] arr = null; System.out.println(arr.length); // 結(jié)果為:Exception in thread "main" java.lang.NullPointerException
此時我們點(diǎn)擊這個異常 NullPointerException
就轉(zhuǎn)到了它的定義,我們會看到
我們可以得到以下結(jié)論:
-
NullPointerException
是一個類 -
這個類繼承了
RuntimeException
這個類
為了刨根究底,我們繼續(xù)轉(zhuǎn)到 RuntimeException
這個類看看
我們又得到了以下結(jié)論:
-
RuntimeException
是一個類 -
這個類繼承了
Exception
這個類
繼續(xù)刨根究底,我們又可以看到
誒,此時我們再對照著體系圖我們就可以理解清除這張圖的所有意思,并且此時對異常又有了個全面對認(rèn)識
而今天我們的主角是異常,即 Exception
,接下來我將會對它進(jìn)行解析。
1.4 異常的核心思想
作為一個程序員,我們經(jīng)常都面對著
錯誤在代碼中的存在我們不言而喻,因此就產(chǎn)生了兩種主要針對錯誤的方式
方式一(LBYL):在操作之前就做充分的檢查
方式二(EAFP):直接操作,有錯誤再解決
而異常的核心思想就是 EAFP
1.5 異常的好處
那么核心思想為 EAFP
的異常有什么好處呢?
我們可以隨便舉一個例子,比如你打一把王者,我們要進(jìn)行登錄、匹配、確認(rèn)游戲、選擇英雄等等的操作。
如果使用 LBYL
風(fēng)格的代碼,我們就要對每一步都做好充分的檢查之后,再進(jìn)行下一步,簡單寫個代碼如下
boolean ret = false; ret = log(); if(!=ret){ // 處理登錄游戲錯誤 return; } ret = matching(); if(!=ret){ // 處理匹配游戲錯誤 return; } ret = confirm(); if(!=ret){ // 處理確認(rèn)游戲錯誤 return; } ret = choose(); if(!=ret){ // 處理選擇游戲錯誤 return; }
而使用 EAFP
的風(fēng)格,代碼則是這樣的
try{ log(); matching(); confirm(); choose(); }catch(登錄游戲異常){ // 處理登錄游戲錯誤 }catch(匹配游戲異常){ // 處理匹配游戲錯誤 }catch(確認(rèn)游戲異常){ // 處理確認(rèn)游戲錯誤 }catch(選擇游戲異常){ // 處理選擇游戲錯誤 }
兩種方式的代碼一對比,大家也可以看得出哪一種更好。EAFP
風(fēng)格的就可以將流程和處理異常的代碼分開,看起來更加舒服。而這也就是使用異常的好處之一。上述代碼運(yùn)用了異常的基本用法,后續(xù)會介紹。
2. 異常的基本用法
2.1 捕獲異常
2.1.1 基本語法
try{ // 有可能出現(xiàn)異常的語句 }[catch(異常類型 異常對象){ // 出現(xiàn)異常后的處理行為 }...] [finally{ // 異常的出口 }]
-
try
代碼塊中放的是可能出現(xiàn)異常的代碼 -
catch
代碼塊中放的是出現(xiàn)異常后的處理行為 -
finally
代碼塊中的代碼用于處理善后工作,會在最后執(zhí)行 -
其中
catch
和finally
都可以根據(jù)情況選擇加或者不加
2.1.2 示例一
首先我們看一個不處理異常的代碼
int[] arr = {1, 2, 3}; System.out.println("before"); System.out.println(arr[100]); System.out.println("after");
結(jié)果是:
我們分析一下這個結(jié)果,首先它告訴我們在
main
方法中出現(xiàn)了數(shù)組越界的異常,原因就是100這個數(shù)字。下面它又告訴我們了這個異常的具體位置。并且通過這個結(jié)果我們知道,當(dāng)代碼出現(xiàn)異常之后,程序就中止了,異常代碼后面的代碼就不會執(zhí)行了。
那么為什么這里拋出異常之后,后面的代碼就不再執(zhí)行了呢?
因?yàn)楫?dāng)沒有處理異常的時候,一旦程序發(fā)生異常,這個異常就會交給 JVM 來處理。
而一旦交給了 JVM 處理異常,程序就會立即終止執(zhí)行!
這也就是為什么我們會有自己處理異常這個行為
我們?nèi)绻由?try catch
自己處理異常
int[] arr = {1, 2, 3}; try { System.out.println("before"); System.out.println(arr[100]); System.out.println("after"); }catch(ArrayIndexOutOfBoundsException e){ System.out.println("數(shù)組越界!"); } System.out.println("after try catch");
結(jié)果是:
我們發(fā)現(xiàn) try
中出現(xiàn)了異常的語句,并且我們針對這個異常做出了處理的行為。而 try catch
后面的程序依然可以繼續(xù)執(zhí)行
我們在上述代碼中處理異常時 catch
里面用的語句就是直接告訴它出現(xiàn)了什么問題,但是如果我們想要知道這是什么異常,在代碼的第幾行有問題的話,就可以再加一個調(diào)用棧。
什么是調(diào)用棧呢?
方法之間存在相互調(diào)用關(guān)系,這種調(diào)用關(guān)系可以用“調(diào)用棧”來描述。在 JVM 中有一塊內(nèi)存空間稱為“虛擬機(jī)棧”,這是專門存儲方法之間調(diào)用關(guān)系的。當(dāng)代碼中出現(xiàn)異常的時候,我們就可使用 e.printStackTrace();
來查看出現(xiàn)異常代碼的調(diào)用棧
2.1.3 示例二(含使用調(diào)用棧)
int[] arr = {1, 2, 3}; try { System.out.println("before"); System.out.println(arr[100]); System.out.println("after"); }catch(ArrayIndexOutOfBoundsException e){ System.out.println("數(shù)組越界!"); e.printStackTrace(); } System.out.println("after try catch");
結(jié)果是:
2.1.4 示例三(可以使用多個 catch 捕獲不同的異常)
int[] arr = {1, 2, 3}; try { System.out.println("before"); System.out.println(arr[100]); System.out.println("after"); }catch(ArrayIndexOutOfBoundsException e){ System.out.println("數(shù)組越界!"); e.printStackTrace(); }catch(NullPointerException e){ System.out.println("空指針異常"); e.printStackTrace(); } System.out.println("after try catch");
這個代碼里面有多個 catch
,他會捕獲到第一個出現(xiàn)異常的位置
2.1.5 示例四(可以使用一個 catch 捕獲所有異常,不推薦)
int[] arr = {1, 2, 3}; try { System.out.println("before"); System.out.println(arr[100]); System.out.println("after"); }catch(Exception e){ e.printStackTrace(); } System.out.println("after try catch");
其中我們使用了
Exception
這個類,我們知道它是所有異常的父類,因此可以用來捕獲所有異常。但是這個方法是不推薦的,因?yàn)楫惓L嗔耍覀儾蝗菀锥ㄎ粏栴}
并且我們能得到一個結(jié)論
catch
進(jìn)行類型匹配的時候,不光會匹配相同類型的異常,也能捕獲目標(biāo)異常類型的子類對象
2.1.6 示例五(使用 finally,它之間的代碼將在 try 語句后執(zhí)行)
int[] arr = {1, 2, 3}; try { arr = null; System.out.println(arr.length); }catch(NullPointerException e){ e.printStackTrace(); }finally{ System.out.println("finally 執(zhí)行啦!"); } System.out.println("after try catch");
結(jié)果為:
我們緊接著再看一個代碼,我將異常給改正確
int[] arr = {1, 2, 3}; try { System.out.println(arr.length); }catch(NullPointerException e){ e.printStackTrace(); }finally{ System.out.println("finally 執(zhí)行啦!"); } System.out.println("after try catch");
上述代碼就沒有錯誤了,但是結(jié)果是
我們就得出了這個結(jié)論
無論
catch
是否捕獲到異常,都要執(zhí)行finally
語句
finally
是用來處理善后工作的,例如釋放資源是可以被做到的。如果大家對于使用 finally
釋放資源有疑惑,可以先看示例八,因?yàn)樵?finally
中加入 Scanner
的 close
方法就是釋放資源的一種例子
2.1.7 示例六(finally 引申的思考題)
public static int func(){ try{ return 10; }catch(NullPointerException e){ e.printStackTrace(); }finally{ return 1; } } public static void main(String[] args) { int num = func(); System.out.println(num); }
結(jié)果為:1
因?yàn)?
finally
塊永遠(yuǎn)是最后執(zhí)行的。并且你也無法在這個代碼之后執(zhí)行其他語句,因?yàn)椴还苡袥]有捕獲到異常都要執(zhí)行finally
中的return
語句去終止代碼
2.1.8 示例七(使用 try 負(fù)責(zé)回收資源)
在演示代碼前要先補(bǔ)充一個關(guān)于 Scanner
的知識
我們知道使用
Scanner
類可以幫助我們進(jìn)行控制臺輸入語句,但是Scanner
還是一種資源,而資源使用完之后是需要回收的,就像是我們打開了一瓶水喝了點(diǎn)還要蓋上它。故用完后我們可以加上close
方法來進(jìn)行回收,如Scanner reader = new Scanner(System.in); int a = reader.nextInt(); reader.close();
而 try 有一種寫法可以在它執(zhí)行完畢后自動調(diào)用 Scanner
的 close
方法
try(Scanner sc = new Scanner(System.in){ int num = sc.nextInt(); }catch(InputMismatchException e){ e.printStackTrace(); }
而這種方式的代碼風(fēng)格要比使用 finally 中含有 close 方法要好些
2.1.9 示例八(本方法中沒有合適的處理異常方式,就會沿著調(diào)用棧向上傳遞)
public static void func(){ int[] arr = {1, 2, 3}; System.out.println(arr[100]); } public static void main(String[] args){ try{ func(); }catch(ArrayIndexOutOfBoundsException e){ e.printStackTrace(); } }
結(jié)果為:
由于我們寫 func
方法時出現(xiàn)了異常沒有及時處理,但我們在 main 方法中調(diào)用它了,所以就經(jīng)過方法之間互相的調(diào)用關(guān)系,我們一直到了 main
方法被調(diào)用的位置,并且此時有合適的處理異常的方法
若最終沒有找到合適的異常處理方法,最終該異常就會交給 JVM 處理,即程序就會終止
2.1.10 異常處理流程總結(jié)
-
程序先執(zhí)行
try
中的代碼 -
如果 try 中的代碼出現(xiàn)異常,就會結(jié)束 try 中異常之后的代碼,并查看該異常和
catch
中的異常類型是否匹配 -
如果匹配,就會執(zhí)行
catch
中的代碼 - 如果沒有匹配的,就會將異常向上傳遞到上層調(diào)用者
-
無論是否找到匹配類型,
finally
中的代碼都會被執(zhí)行 - 如果上層調(diào)用者沒有處理異常的方法,就會繼續(xù)向上傳遞
-
一直到 main 方法也沒有合適的代碼處理異常,就會交給
JVM
來處理,此時程序就會終止
2.2 拋出異常
以上我們介紹的都是 Java
內(nèi)置的類拋出的一些異常,除此之外我們也可以使用關(guān)鍵字 throw 手動拋出一個異常,如
public static int divide(int x, int y) { if (y == 0) { throw new ArithmeticException("拋出除 0 異常"); } } public static void main(String[] args) { System.out.println(divide(10, 0)); }
該代碼就是我們手動拋出的異常,并且手動拋出的異常還可以使用自定義的異常,后面將會介紹到
2.3 異常說明
我們在處理異常時,如果有一個方法,里面很長一大段,我們其實(shí)是希望很簡單的就知道這段代碼有可能會出現(xiàn)哪些異常。故我們可以使用關(guān)鍵字 throws
,把可能拋出的異常顯示的標(biāo)注在方法定義的位置,從而提醒使用者要注意捕獲這些異常,如
public static int divide(int x, int y) throws ArithmeticException{ if (y == 0) { throw new ArithmeticException("拋出除 0 異常"); } }
注意:
如果我們將
main
方法拋出一個異常說明,而main
方法的調(diào)用者是JVM
,所以如果在mai
n 函數(shù)上拋出異常的話,就相當(dāng)于JVM
來處理這個異常了
3. 自定義異常類
Java
中雖然有豐富的異常類,但是實(shí)際上肯定還要一些情況需要我們對這些異常進(jìn)行擴(kuò)展,創(chuàng)建新的符合情景的異常。
那怎么創(chuàng)建自定義異常呢?首先我們就可以去看看原有的那些異常是怎么做的
兩異常
我們發(fā)現(xiàn)這兩個異常都是繼承在
RuntimeException
這個類的,并且都構(gòu)造了兩個構(gòu)造方法,分別是不帶參數(shù)和帶參數(shù)
而我模擬了一個登錄賬號的代碼
public class TestDemo { private static String userName = "root"; private static String password = "123456"; public static void main(String[] args) { login("admin", "123456"); } public static void login(String userName, String password) { if (!TestDemo.userName.equals(userName)) { // 處理用戶名錯誤 } if (!TestDemo.password.equals(password)) { // 處理密碼錯誤 } System.out.println("登陸成功"); } }
通過這個模擬的場景,我們可以針對運(yùn)行時賬號和密碼是否正確寫一個異常
class UserException extends RuntimeException{ public UserException(){ super(); } public UserException(String s){ super(s); } } class PasswordException extends RuntimeException{ public PasswordException(){ super(); } public PasswordException(String s){ super(s); } }
緊接著我們再手動拋出異常
public class TestDemo { private static String userName = "root"; private static String password = "123456"; public static void main(String[] args) { login("admin", "123456"); } public static void login(String userName, String password) { if (!TestDemo.userName.equals(userName)) { throws new UserException("用戶名錯誤"); } if (!TestDemo.password.equals(password)) { throws new PasswordException("密碼錯誤"); } System.out.println("登陸成功"); } }
所以我們創(chuàng)建新的異常時,就是先思考這是哪種類型的異常,再照貓畫虎。但是可能有疑惑,如果我們新建異常時統(tǒng)一繼承 Exception
不就行嗎?
No!由于
Exception
分為編譯時異常和運(yùn)行時異常,使用Exception
的話默認(rèn)是編譯時異常(即受查異常),而一段代碼可能拋出受查異常則必須顯示進(jìn)行處理。
故如果我們將上述新建的異常繼承 Exception
的話,就要再對代碼中的異常進(jìn)行處理,否則會直接報錯
到此這篇關(guān)于Java 基礎(chǔ)語法 異常處理的文章就介紹到這了,更多相關(guān)Java
異常處理內(nèi)容請搜索服務(wù)器之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持服務(wù)器之家!
原文鏈接:https://blog.csdn.net/weixin_51367845/article/details/120610411