Java異常處理機制深入理解
1.初識異常
我們在寫代碼的時候都或多或少碰到了大大小小的異常,例如:
public class Test {
public static void main(String[] args) {
int[] arr = {1,2,3};
System.out.println(arr[5]);
}
}當我們數(shù)組越界時,編譯器會給我們報數(shù)組越界,并提示哪行出了錯。

再比如:
class Test{
int num = 10;
public static void main(String[] args) {
Test test = null;
System.out.println(test.num);
}
}當我們嘗試用使用空對象時,編譯器也會報空指針異常:

那么究竟什么是異常?
所謂異常指的就是程序在 運行時 出現(xiàn)錯誤時通知調(diào)用者的一種機制 .
關(guān)鍵字 "運行時" ,有些錯誤是這樣的, 例如將 System.out.println 拼寫錯了, 寫成了
system.out.println. 此時編譯過程中就會出 錯, 這是 "編譯期" 出錯.
而運行時指的是程序已經(jīng)編譯通過得到 class 文件了 , 再由 JVM 執(zhí)行過程中出現(xiàn)的錯誤 .
2.異常的基本用法
Java異常處理依賴于5個關(guān)鍵字:try、catch、finally、throws、throw。下面來逐一介紹下。
①try:try塊中主要放置可能會產(chǎn)生異常的代碼塊。如果執(zhí)行try塊里的業(yè)務邏輯代碼時出現(xiàn)異
常,系統(tǒng)會自動生成一個異常對象,該異常對象被提交給運行環(huán)境,這個過程被稱為拋出
(throw)異常。Java環(huán)境收到異常對象時,會尋找合適的catch塊(在本方法或是調(diào)用方
法)。
②catch: catch 代碼塊中放的是出現(xiàn)異常后的處理行為,也可以寫此異常出錯的原因或者打
印棧上的錯誤信息。但catch語句不能為空,因為一旦將catch語句寫為空,就代表忽略了此
異常。如:

空的catch塊會使異常達不到應有的目的,即強迫你處理異常的情況。忽略異常就如同忽略
火警信號一樣——若把火警信號關(guān)掉了,當真正的火災發(fā)生時,就沒有人能看到火警信號
了?;蛟S你會僥幸逃過一劫,或許結(jié)果將是災難性的。每當見到空的catch塊時,我們都應該
警鐘長鳴。
當然也有一種情況可以忽略異常,即關(guān)閉fileinputstream(讀寫本地文件)的時候。因為你還
沒有改變文件的狀態(tài),因此不必執(zhí)行任何恢復動作,并且已經(jīng)從文件中讀取到所需要的信
息,因此不必終止正在進行的操作。
③finally:finally 代碼塊中的代碼用于處理善后工作, 會在最后執(zhí)行,也一定會被執(zhí)行。當遇
到try或catch中return或throw之類可以終止當前方法的代碼時,jvm會先去執(zhí)行finally中的語
句,當finally中的語句執(zhí)行完畢后才會返回來執(zhí)行try/catch中的return,throw語句。如果
finally中有return或throw,那么將執(zhí)行這些語句,不會在執(zhí)行try/catch中的return或throw語
句。finally塊中一般寫的是關(guān)閉資源之類的代碼。但是我們一般不在finally語句中加入return
語句,因為他會覆蓋掉try中執(zhí)行的return語句。例如:

finally將最后try執(zhí)行的return 10覆蓋了,最后結(jié)果返回了20.
④throws:在方法的簽名中,用于拋出此方法中的異常給調(diào)用者,調(diào)用者可以選擇捕獲或者
拋出,如果所有方法(包括main)都選擇拋出(或者沒有合適的處理異常的方式,即異常類
型不匹配)那么最終將會拋給JVM,就會像我們之前沒使用try、catch語句一樣。JVM打印出
棧軌跡(異常鏈)。
⑤throw:用于拋出一個具體的異常對象。常用于自定義異常類中。
ps:
關(guān)于 "調(diào)用棧",方法之間是存在相互調(diào)用關(guān)系的, 這種調(diào)用關(guān)系我們可以用 "調(diào)用棧" 來描述.
在 JVM 中有一塊內(nèi)存空間稱為 "虛擬機棧" 專門存儲方法之間的調(diào)用關(guān)系. 當代碼中出現(xiàn)異常
的時候, 我們就可以使用 e.printStackTrace() 的方式查看出現(xiàn)異常代碼的調(diào)用棧,一般寫在catch語句中。
異常處理流程
- 程序先執(zhí)行 try 中的代碼
- 如果 try 中的代碼出現(xiàn)異常, 就會結(jié)束 try 中的代碼, 看和 catch 中的異常類型是否匹配.
- 如果找到匹配的異常類型, 就會執(zhí)行 catch 中的代碼
- 如果沒有找到匹配的異常類型, 就會將異常向上傳遞到上層調(diào)用者.
- 無論是否找到匹配的異常類型, finally 中的代碼都會被執(zhí)行到(在該方法結(jié)束之前執(zhí)行).
- 如果上層調(diào)用者也沒有處理的了異常, 就繼續(xù)向上傳遞.
- 一直到 main 方法也沒有合適的代碼處理異常, 就會交給 JVM 來進行處理, 此時程序就會異常終止.
3.為什么要使用異常?
存在即合理,舉個例子
//不使用異常
int[] arr = {1, 2, 3};
System.out.println("before");
System.out.println(arr[100]);
System.out.println("after");當我們不使用異常時,發(fā)現(xiàn)出現(xiàn)異常程序直接崩潰,后面的after也沒有打印。

//使用異常
int[] arr = {1, 2, 3};
try {
System.out.println("before");
System.out.println(arr[100]);
System.out.println("after");
} catch (ArrayIndexOutOfBoundsException e) {
// 打印出現(xiàn)異常的調(diào)用棧
e.printStackTrace();
}
System.out.println("after try catch");當我們使用了異常,雖然after也沒有執(zhí)行,但程序并沒有直接崩潰,后面的sout語句還是執(zhí)行了

這不就是異常的作用所在嗎?
再舉個例子,當玩王者榮耀時,突然斷網(wǎng),他不會讓你直接程序崩潰吧,而是給你斷線重連的機會吧:

我們再用偽代碼演示一把王者榮耀的對局過程:
不使用異常處理
boolean ret = false;
ret = 登陸游戲();
if (!ret) {
處理登陸游戲錯誤;
return;
}
ret = 開始匹配();
if (!ret) {
處理匹配錯誤;
return;
}
ret = 游戲確認();
if (!ret) {
處理游戲確認錯誤;
return;
}
ret = 選擇英雄();
if (!ret) {
處理選擇英雄錯誤;
return;
}
ret = 載入游戲畫面();
if (!ret) {
處理載入游戲錯誤;
return;
}
......
使用異常處理
try {
登陸游戲();
開始匹配();
游戲確認();
選擇英雄();
載入游戲畫面();
...
} catch (登陸游戲異常) {
處理登陸游戲異常;
} catch (開始匹配異常) {
處理開始匹配異常;
} catch (游戲確認異常) {
處理游戲確認異常;
} catch (選擇英雄異常) {
處理選擇英雄異常;
} catch (載入游戲畫面異常) {
處理載入游戲畫面異常;
}
......我們能明顯的看到不使用異常時,正確流程和錯誤處理代碼混在一起,不易于分辨,而用了
異常后,能更易于理解代碼。
當然使用異常的好處還遠不止于此,我們可以在try、catch語句中加入信息提醒功能,比如你
開發(fā)了一個軟件,當那個軟件出現(xiàn)異常時,發(fā)個信息提醒你及時去修復。博主就做了一個小
小的qq郵箱信息提醒功能,源碼在碼云,有興趣的可以去看看呀!需要配置qq郵箱pop3服
務,友友們可以去查查怎么開啟呀,我們主旨不是這個所以不教怎么開啟了。演示一下:

別群發(fā)消息哦,不然可能會被封號???
異常應只用于異常的情況
try{
int i = 0;
while(true)
System.out.println(a[i++]);
}catch(ArrayIndexOutOfBoundsException e){
}這段代碼有什么用?看起來根本不明顯,這正是它沒有真正被使用的原因。事實證明,作為
一個要對數(shù)組元素進行遍歷的實現(xiàn)方式,它的構(gòu)想是非常拙劣的。當這個循環(huán)企圖訪問數(shù)組
邊界之外的第一個數(shù)組元素時,用拋出(throw)、捕獲(catch)、
忽略(ArrayIndexOutOfBoundsException)的手段來達到終止無限循環(huán)的目的。假定它與數(shù)
組循環(huán)是等價的,對于任何一個Java程序員來講,下面的標準模式一看就會明白:
for(int m : a) System.out.println(m);
為什么優(yōu)先異常的模式,而不是用行之有效標準模式呢?
可能是被誤導了,企圖利用異常機制提高性能,因為jvm每次訪問數(shù)組都需要判斷下標是否越
界,他們認為循環(huán)終止被隱藏了,但是在foreach循環(huán)中仍然可見,這無疑是多余的,應該避
免。
上面想法有三個錯誤:
1.異常機制設計的初衷是用來處理不正常的情況,所以JVM很少對它們進行優(yōu)化。
2.代碼放在try…catch中反而阻止jvm本身要執(zhí)行的某些特定優(yōu)化。
3.對數(shù)組進行遍歷的標準模式并不會導致冗余的檢查。
這個例子的教訓很簡單:顧名思義,異常應只用于異常的情況下,它們永遠不應該用于正常
的控制流。
總結(jié):異常是為了在異常情況下使用而設計的,不要用于一般的控制語句。
4. 異常的種類
在Java中提供了三種可拋出結(jié)構(gòu):受查異常(checked exception)、運行時異常(run-time exception)和錯誤(error)。


(補充)
4.1 受查異常
什么是受查異常?只要不是派生于error或runtime的異常類都是受查異常。舉個例子:
我們自定義兩個異常類和一個接口,以及一個測試類
interface IUser {
void changePwd() throws SafeException,RejectException;
}
class SafeException extends Exception {//因為繼承的是execption,所以是受查異常類
public SafeException() {
}
public SafeException(String message) {
super(message);
}
}
class RejectException extends Exception {//因為繼承的是execption,所以是受查異常類
public RejectException() {
}
public RejectException(String message) {
super(message);
}
}
public class Test {
public static void main(String[] args) {
IUser user = null;
user.changePwd();
}
}
我們發(fā)現(xiàn)test測試類中user使用方法報錯了,因為java認為checked異常都是可以再編譯階
段被處理的異常,所以它強制程序處理所有的checked異常,java程序必須顯式處checked
異常,如果程序沒有處理,則在編譯時會發(fā)生錯誤,無法通過編譯。
解決方案:
①try、catch包裹
IUser user = null;
try {
user.changePwd();
}catch (SafeException e){
e.printStackTrace();
}
catch (RejectException e){
e.printStackTrace();
}
②拋出異常,將處理動作交給上級調(diào)用者,調(diào)用者在調(diào)用這個方法時還是要寫一遍try、catch
包裹語句的,所以這個其實是相當于聲明,讓調(diào)用者知道這個函數(shù)需要拋出異常
public static void main(String[] args) throws SafeException, RejectException {
IUser user = null;
user.changePwd();
}
4.2非受查異常
派生于error或runtime類的所有異常類就是非受查異常。
可以這么說,我們現(xiàn)在寫程序遇到的異常大部分都是非受查異常,程序直接崩潰,后面的也
不執(zhí)行。

像空指針異常、數(shù)組越界異常、算術(shù)異常等,都是非受查異常。由編譯器運行時給你檢查出
來的,所以也叫作運行時異常。
5.如何使用異常
避免不必要的使用受查異常
如果不能阻止異常條件的產(chǎn)生,并且一旦產(chǎn)生異常,程序員可以立即采取有用的動作,這種
受查異常才是可取的。否則,更適合用非受查異常。這種例子就是
CloneNotSuppportedException(受查異常)。它是被Object.clone拋出來的,Object.clone
只有在實現(xiàn)了Cloneable的對象上才可以被調(diào)用。


被一個方法單獨拋出的受查異常,會給程序員帶來非常高的額外負擔,如果這個方法還有其
他的受查異常,那么它被調(diào)用是一定已經(jīng)出現(xiàn)在一個try塊中,所以這個異常只需要另外一個
catch塊。但當只拋出一個受查異常時,僅僅一個異常就會導致該方法不得不處于try塊中,也
就導致了使用這個方法的類都不得不使用try、catch語句,使代碼可讀性也變低了。
受查異常使接口聲明脆弱,比如一開始一個接口只有一個聲明異常
interfaceUser{
//修改用戶名,拋出安全異常
publicvoid changePassword() throws MySecurityExcepiton;
} 但隨著系統(tǒng)開發(fā),實現(xiàn)接口的類越來越多,突然發(fā)現(xiàn)changePassword還需要拋出另一個異
常,那么實現(xiàn)這個接口的所有類也都要追加對這個新異常的處理,這個工程量就很大了。
總結(jié):如果不是非用不可,盡量使用非受查異常,或?qū)⑹懿楫惓^D(zhuǎn)為非受查異常。
6.自定義異常
我們用自定義異常來實現(xiàn)一個登錄報錯的小應用
class NameException extends RuntimeException{//用戶名錯誤異常
public NameException(String message){
super(message);
}
}
class PasswordException extends RuntimeException{//密碼錯誤異常
public PasswordException(String message){
super(message);
}
}test類來測試運行
public class Test {
private static final String name = "bit";
private static final String password ="123";
public static void Login(String name,String password) throws NameException,PasswordException{
try{
if(!Test.name.equals(name)){
throw new NameException("用戶名錯誤!");
}
}catch (NameException e){
e.printStackTrace();
}
try {
if(!Test.password.equals(password)){
throw new PasswordException("密碼錯誤");
}
}catch (PasswordException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String name = scanner.nextLine();
String password = scanner.nextLine();
Login(name,password);
}
}


關(guān)于異常就到此為止了,怎么感覺還有點意猶未盡呢?
到此這篇關(guān)于Java異常處理機制深入理解的文章就介紹到這了,更多相關(guān)Java 異常處理機制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java數(shù)據(jù)結(jié)構(gòu)基礎:稀疏數(shù)組
今天帶大家了解一下Java稀疏數(shù)組的相關(guān)知識,文中有非常詳細的介紹及代碼示例,對正在學習java的小伙伴們有很好地幫助,需要的朋友可以參考下2021-08-08
Java使用RedisTemplate如何根據(jù)前綴獲取key列表
這篇文章主要介紹了Java使用RedisTemplate如何根據(jù)前綴獲取key列表,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-06-06

