Java異常處理機(jī)制深入理解
1.初識(shí)異常
我們?cè)趯?xiě)代碼的時(shí)候都或多或少碰到了大大小小的異常,例如:
public class Test { public static void main(String[] args) { int[] arr = {1,2,3}; System.out.println(arr[5]); } }
當(dāng)我們數(shù)組越界時(shí),編譯器會(huì)給我們報(bào)數(shù)組越界,并提示哪行出了錯(cuò)。
再比如:
class Test{ int num = 10; public static void main(String[] args) { Test test = null; System.out.println(test.num); } }
當(dāng)我們嘗試用使用空對(duì)象時(shí),編譯器也會(huì)報(bào)空指針異常:
那么究竟什么是異常?
所謂異常指的就是程序在 運(yùn)行時(shí) 出現(xiàn)錯(cuò)誤時(shí)通知調(diào)用者的一種機(jī)制 .
關(guān)鍵字 "運(yùn)行時(shí)" ,有些錯(cuò)誤是這樣的, 例如將 System.out.println 拼寫(xiě)錯(cuò)了, 寫(xiě)成了
system.out.println. 此時(shí)編譯過(guò)程中就會(huì)出 錯(cuò), 這是 "編譯期" 出錯(cuò).
而運(yùn)行時(shí)指的是程序已經(jīng)編譯通過(guò)得到 class 文件了 , 再由 JVM 執(zhí)行過(guò)程中出現(xiàn)的錯(cuò)誤 .
2.異常的基本用法
Java異常處理依賴(lài)于5個(gè)關(guān)鍵字:try、catch、finally、throws、throw。下面來(lái)逐一介紹下。
①try:try塊中主要放置可能會(huì)產(chǎn)生異常的代碼塊。如果執(zhí)行try塊里的業(yè)務(wù)邏輯代碼時(shí)出現(xiàn)異
常,系統(tǒng)會(huì)自動(dòng)生成一個(gè)異常對(duì)象,該異常對(duì)象被提交給運(yùn)行環(huán)境,這個(gè)過(guò)程被稱(chēng)為拋出
(throw)異常。Java環(huán)境收到異常對(duì)象時(shí),會(huì)尋找合適的catch塊(在本方法或是調(diào)用方
法)。
②catch: catch 代碼塊中放的是出現(xiàn)異常后的處理行為,也可以寫(xiě)此異常出錯(cuò)的原因或者打
印棧上的錯(cuò)誤信息。但catch語(yǔ)句不能為空,因?yàn)橐坏atch語(yǔ)句寫(xiě)為空,就代表忽略了此
異常。如:
空的catch塊會(huì)使異常達(dá)不到應(yīng)有的目的,即強(qiáng)迫你處理異常的情況。忽略異常就如同忽略
火警信號(hào)一樣——若把火警信號(hào)關(guān)掉了,當(dāng)真正的火災(zāi)發(fā)生時(shí),就沒(méi)有人能看到火警信號(hào)
了?;蛟S你會(huì)僥幸逃過(guò)一劫,或許結(jié)果將是災(zāi)難性的。每當(dāng)見(jiàn)到空的catch塊時(shí),我們都應(yīng)該
警鐘長(zhǎng)鳴。
當(dāng)然也有一種情況可以忽略異常,即關(guān)閉fileinputstream(讀寫(xiě)本地文件)的時(shí)候。因?yàn)槟氵€
沒(méi)有改變文件的狀態(tài),因此不必執(zhí)行任何恢復(fù)動(dòng)作,并且已經(jīng)從文件中讀取到所需要的信
息,因此不必終止正在進(jìn)行的操作。
③finally:finally 代碼塊中的代碼用于處理善后工作, 會(huì)在最后執(zhí)行,也一定會(huì)被執(zhí)行。當(dāng)遇
到try或catch中return或throw之類(lèi)可以終止當(dāng)前方法的代碼時(shí),jvm會(huì)先去執(zhí)行finally中的語(yǔ)
句,當(dāng)finally中的語(yǔ)句執(zhí)行完畢后才會(huì)返回來(lái)執(zhí)行try/catch中的return,throw語(yǔ)句。如果
finally中有return或throw,那么將執(zhí)行這些語(yǔ)句,不會(huì)在執(zhí)行try/catch中的return或throw語(yǔ)
句。finally塊中一般寫(xiě)的是關(guān)閉資源之類(lèi)的代碼。但是我們一般不在finally語(yǔ)句中加入return
語(yǔ)句,因?yàn)樗麜?huì)覆蓋掉try中執(zhí)行的return語(yǔ)句。例如:
finally將最后try執(zhí)行的return 10覆蓋了,最后結(jié)果返回了20.
④throws:在方法的簽名中,用于拋出此方法中的異常給調(diào)用者,調(diào)用者可以選擇捕獲或者
拋出,如果所有方法(包括main)都選擇拋出(或者沒(méi)有合適的處理異常的方式,即異常類(lèi)
型不匹配)那么最終將會(huì)拋給JVM,就會(huì)像我們之前沒(méi)使用try、catch語(yǔ)句一樣。JVM打印出
棧軌跡(異常鏈)。
⑤throw:用于拋出一個(gè)具體的異常對(duì)象。常用于自定義異常類(lèi)中。
ps:
關(guān)于 "調(diào)用棧",方法之間是存在相互調(diào)用關(guān)系的, 這種調(diào)用關(guān)系我們可以用 "調(diào)用棧" 來(lái)描述.
在 JVM 中有一塊內(nèi)存空間稱(chēng)為 "虛擬機(jī)棧" 專(zhuān)門(mén)存儲(chǔ)方法之間的調(diào)用關(guān)系. 當(dāng)代碼中出現(xiàn)異常
的時(shí)候, 我們就可以使用 e.printStackTrace() 的方式查看出現(xiàn)異常代碼的調(diào)用棧,一般寫(xiě)在catch語(yǔ)句中。
異常處理流程
- 程序先執(zhí)行 try 中的代碼
- 如果 try 中的代碼出現(xiàn)異常, 就會(huì)結(jié)束 try 中的代碼, 看和 catch 中的異常類(lèi)型是否匹配.
- 如果找到匹配的異常類(lèi)型, 就會(huì)執(zhí)行 catch 中的代碼
- 如果沒(méi)有找到匹配的異常類(lèi)型, 就會(huì)將異常向上傳遞到上層調(diào)用者.
- 無(wú)論是否找到匹配的異常類(lèi)型, finally 中的代碼都會(huì)被執(zhí)行到(在該方法結(jié)束之前執(zhí)行).
- 如果上層調(diào)用者也沒(méi)有處理的了異常, 就繼續(xù)向上傳遞.
- 一直到 main 方法也沒(méi)有合適的代碼處理異常, 就會(huì)交給 JVM 來(lái)進(jìn)行處理, 此時(shí)程序就會(huì)異常終止.
3.為什么要使用異常?
存在即合理,舉個(gè)例子
//不使用異常 int[] arr = {1, 2, 3}; System.out.println("before"); System.out.println(arr[100]); System.out.println("after");
當(dāng)我們不使用異常時(shí),發(fā)現(xiàn)出現(xiàn)異常程序直接崩潰,后面的after也沒(méi)有打印。
//使用異常 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");
當(dāng)我們使用了異常,雖然after也沒(méi)有執(zhí)行,但程序并沒(méi)有直接崩潰,后面的sout語(yǔ)句還是執(zhí)行了
這不就是異常的作用所在嗎?
再舉個(gè)例子,當(dāng)玩王者榮耀時(shí),突然斷網(wǎng),他不會(huì)讓你直接程序崩潰吧,而是給你斷線重連的機(jī)會(huì)吧:
我們?cè)儆脗未a演示一把王者榮耀的對(duì)局過(guò)程:
不使用異常處理 boolean ret = false; ret = 登陸游戲(); if (!ret) { 處理登陸游戲錯(cuò)誤; return; } ret = 開(kāi)始匹配(); if (!ret) { 處理匹配錯(cuò)誤; return; } ret = 游戲確認(rèn)(); if (!ret) { 處理游戲確認(rèn)錯(cuò)誤; return; } ret = 選擇英雄(); if (!ret) { 處理選擇英雄錯(cuò)誤; return; } ret = 載入游戲畫(huà)面(); if (!ret) { 處理載入游戲錯(cuò)誤; return; } ......
使用異常處理 try { 登陸游戲(); 開(kāi)始匹配(); 游戲確認(rèn)(); 選擇英雄(); 載入游戲畫(huà)面(); ... } catch (登陸游戲異常) { 處理登陸游戲異常; } catch (開(kāi)始匹配異常) { 處理開(kāi)始匹配異常; } catch (游戲確認(rèn)異常) { 處理游戲確認(rèn)異常; } catch (選擇英雄異常) { 處理選擇英雄異常; } catch (載入游戲畫(huà)面異常) { 處理載入游戲畫(huà)面異常; } ......
我們能明顯的看到不使用異常時(shí),正確流程和錯(cuò)誤處理代碼混在一起,不易于分辨,而用了
異常后,能更易于理解代碼。
當(dāng)然使用異常的好處還遠(yuǎn)不止于此,我們可以在try、catch語(yǔ)句中加入信息提醒功能,比如你
開(kāi)發(fā)了一個(gè)軟件,當(dāng)那個(gè)軟件出現(xiàn)異常時(shí),發(fā)個(gè)信息提醒你及時(shí)去修復(fù)。博主就做了一個(gè)小
小的qq郵箱信息提醒功能,源碼在碼云,有興趣的可以去看看呀!需要配置qq郵箱pop3服
務(wù),友友們可以去查查怎么開(kāi)啟呀,我們主旨不是這個(gè)所以不教怎么開(kāi)啟了。演示一下:
別群發(fā)消息哦,不然可能會(huì)被封號(hào)???
異常應(yīng)只用于異常的情況
try{ int i = 0; while(true) System.out.println(a[i++]); }catch(ArrayIndexOutOfBoundsException e){ }
這段代碼有什么用?看起來(lái)根本不明顯,這正是它沒(méi)有真正被使用的原因。事實(shí)證明,作為
一個(gè)要對(duì)數(shù)組元素進(jìn)行遍歷的實(shí)現(xiàn)方式,它的構(gòu)想是非常拙劣的。當(dāng)這個(gè)循環(huán)企圖訪問(wèn)數(shù)組
邊界之外的第一個(gè)數(shù)組元素時(shí),用拋出(throw)、捕獲(catch)、
忽略(ArrayIndexOutOfBoundsException)的手段來(lái)達(dá)到終止無(wú)限循環(huán)的目的。假定它與數(shù)
組循環(huán)是等價(jià)的,對(duì)于任何一個(gè)Java程序員來(lái)講,下面的標(biāo)準(zhǔn)模式一看就會(huì)明白:
for(int m : a) System.out.println(m);
為什么優(yōu)先異常的模式,而不是用行之有效標(biāo)準(zhǔn)模式呢?
可能是被誤導(dǎo)了,企圖利用異常機(jī)制提高性能,因?yàn)閖vm每次訪問(wèn)數(shù)組都需要判斷下標(biāo)是否越
界,他們認(rèn)為循環(huán)終止被隱藏了,但是在foreach循環(huán)中仍然可見(jiàn),這無(wú)疑是多余的,應(yīng)該避
免。
上面想法有三個(gè)錯(cuò)誤:
1.異常機(jī)制設(shè)計(jì)的初衷是用來(lái)處理不正常的情況,所以JVM很少對(duì)它們進(jìn)行優(yōu)化。
2.代碼放在try…catch中反而阻止jvm本身要執(zhí)行的某些特定優(yōu)化。
3.對(duì)數(shù)組進(jìn)行遍歷的標(biāo)準(zhǔn)模式并不會(huì)導(dǎo)致冗余的檢查。
這個(gè)例子的教訓(xùn)很簡(jiǎn)單:顧名思義,異常應(yīng)只用于異常的情況下,它們永遠(yuǎn)不應(yīng)該用于正常
的控制流。
總結(jié):異常是為了在異常情況下使用而設(shè)計(jì)的,不要用于一般的控制語(yǔ)句。
4. 異常的種類(lèi)
在Java中提供了三種可拋出結(jié)構(gòu):受查異常(checked exception)、運(yùn)行時(shí)異常(run-time exception)和錯(cuò)誤(error)。
(補(bǔ)充)
4.1 受查異常
什么是受查異常?只要不是派生于error或runtime的異常類(lèi)都是受查異常。舉個(gè)例子:
我們自定義兩個(gè)異常類(lèi)和一個(gè)接口,以及一個(gè)測(cè)試類(lèi)
interface IUser { void changePwd() throws SafeException,RejectException; } class SafeException extends Exception {//因?yàn)槔^承的是execption,所以是受查異常類(lèi) public SafeException() { } public SafeException(String message) { super(message); } } class RejectException extends Exception {//因?yàn)槔^承的是execption,所以是受查異常類(lèi) 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測(cè)試類(lèi)中user使用方法報(bào)錯(cuò)了,因?yàn)閖ava認(rèn)為checked異常都是可以再編譯階
段被處理的異常,所以它強(qiáng)制程序處理所有的checked異常,java程序必須顯式處checked
異常,如果程序沒(méi)有處理,則在編譯時(shí)會(huì)發(fā)生錯(cuò)誤,無(wú)法通過(guò)編譯。
解決方案:
①try、catch包裹
IUser user = null; try { user.changePwd(); }catch (SafeException e){ e.printStackTrace(); } catch (RejectException e){ e.printStackTrace(); }
②拋出異常,將處理動(dòng)作交給上級(jí)調(diào)用者,調(diào)用者在調(diào)用這個(gè)方法時(shí)還是要寫(xiě)一遍try、catch
包裹語(yǔ)句的,所以這個(gè)其實(shí)是相當(dāng)于聲明,讓調(diào)用者知道這個(gè)函數(shù)需要拋出異常
public static void main(String[] args) throws SafeException, RejectException { IUser user = null; user.changePwd(); }
4.2非受查異常
派生于error或runtime類(lèi)的所有異常類(lèi)就是非受查異常。
可以這么說(shuō),我們現(xiàn)在寫(xiě)程序遇到的異常大部分都是非受查異常,程序直接崩潰,后面的也
不執(zhí)行。
像空指針異常、數(shù)組越界異常、算術(shù)異常等,都是非受查異常。由編譯器運(yùn)行時(shí)給你檢查出
來(lái)的,所以也叫作運(yùn)行時(shí)異常。
5.如何使用異常
避免不必要的使用受查異常
如果不能阻止異常條件的產(chǎn)生,并且一旦產(chǎn)生異常,程序員可以立即采取有用的動(dòng)作,這種
受查異常才是可取的。否則,更適合用非受查異常。這種例子就是
CloneNotSuppportedException(受查異常)。它是被Object.clone拋出來(lái)的,Object.clone
只有在實(shí)現(xiàn)了Cloneable的對(duì)象上才可以被調(diào)用。
被一個(gè)方法單獨(dú)拋出的受查異常,會(huì)給程序員帶來(lái)非常高的額外負(fù)擔(dān),如果這個(gè)方法還有其
他的受查異常,那么它被調(diào)用是一定已經(jīng)出現(xiàn)在一個(gè)try塊中,所以這個(gè)異常只需要另外一個(gè)
catch塊。但當(dāng)只拋出一個(gè)受查異常時(shí),僅僅一個(gè)異常就會(huì)導(dǎo)致該方法不得不處于try塊中,也
就導(dǎo)致了使用這個(gè)方法的類(lèi)都不得不使用try、catch語(yǔ)句,使代碼可讀性也變低了。
受查異常使接口聲明脆弱,比如一開(kāi)始一個(gè)接口只有一個(gè)聲明異常
interfaceUser{ //修改用戶名,拋出安全異常 publicvoid changePassword() throws MySecurityExcepiton; }
但隨著系統(tǒng)開(kāi)發(fā),實(shí)現(xiàn)接口的類(lèi)越來(lái)越多,突然發(fā)現(xiàn)changePassword還需要拋出另一個(gè)異
常,那么實(shí)現(xiàn)這個(gè)接口的所有類(lèi)也都要追加對(duì)這個(gè)新異常的處理,這個(gè)工程量就很大了。
總結(jié):如果不是非用不可,盡量使用非受查異常,或?qū)⑹懿楫惓^D(zhuǎn)為非受查異常。
6.自定義異常
我們用自定義異常來(lái)實(shí)現(xiàn)一個(gè)登錄報(bào)錯(cuò)的小應(yīng)用
class NameException extends RuntimeException{//用戶名錯(cuò)誤異常 public NameException(String message){ super(message); } } class PasswordException extends RuntimeException{//密碼錯(cuò)誤異常 public PasswordException(String message){ super(message); } }
test類(lèi)來(lái)測(cè)試運(yùn)行
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("用戶名錯(cuò)誤!"); } }catch (NameException e){ e.printStackTrace(); } try { if(!Test.password.equals(password)){ throw new PasswordException("密碼錯(cuò)誤"); } }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)于異常就到此為止了,怎么感覺(jué)還有點(diǎn)意猶未盡呢?
到此這篇關(guān)于Java異常處理機(jī)制深入理解的文章就介紹到這了,更多相關(guān)Java 異常處理機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java數(shù)據(jù)結(jié)構(gòu)基礎(chǔ):稀疏數(shù)組
今天帶大家了解一下Java稀疏數(shù)組的相關(guān)知識(shí),文中有非常詳細(xì)的介紹及代碼示例,對(duì)正在學(xué)習(xí)java的小伙伴們有很好地幫助,需要的朋友可以參考下2021-08-08Spring使用注解實(shí)現(xiàn)Bean的自動(dòng)裝配
大家好,本篇文章主要講的是Spring使用注解實(shí)現(xiàn)Bean的自動(dòng)裝配,感興趣的同學(xué)趕快來(lái)看一看吧,對(duì)你有幫助的話記得收藏一下2022-02-02Java使用RedisTemplate如何根據(jù)前綴獲取key列表
這篇文章主要介紹了Java使用RedisTemplate如何根據(jù)前綴獲取key列表,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06