Java異常處理機制深入理解
1.初識異常
我們在寫代碼的時候都或多或少碰到瞭大大小小的異常,例如:
public class Test { public static void main(String[] args) { int[] arr = {1,2,3}; System.out.println(arr[5]); } }
當我們數組越界時,編譯器會給我們報數組越界,並提示哪行出瞭錯。
再比如:
class Test{ int num = 10; public static void main(String[] args) { Test test = null; System.out.println(test.num); } }
當我們嘗試用使用空對象時,編譯器也會報空指針異常:
那麼究竟什麼是異常?
所謂異常指的就是程序在 運行時 出現錯誤時通知調用者的一種機制 .
關鍵字 "運行時" ,有些錯誤是這樣的, 例如將 System.out.println 拼寫錯瞭, 寫成瞭
system.out.println. 此時編譯過程中就會出 錯, 這是 "編譯期" 出錯.
而運行時指的是程序已經編譯通過得到 class 文件瞭 , 再由 JVM 執行過程中出現的錯誤 .
2.異常的基本用法
Java異常處理依賴於5個關鍵字:try、catch、finally、throws、throw。下面來逐一介紹下。
①try:try塊中主要放置可能會產生異常的代碼塊。如果執行try塊裡的業務邏輯代碼時出現異
常,系統會自動生成一個異常對象,該異常對象被提交給運行環境,這個過程被稱為拋出
(throw)異常。Java環境收到異常對象時,會尋找合適的catch塊(在本方法或是調用方
法)。
②catch: catch 代碼塊中放的是出現異常後的處理行為,也可以寫此異常出錯的原因或者打
印棧上的錯誤信息。但catch語句不能為空,因為一旦將catch語句寫為空,就代表忽略瞭此
異常。如:
空的catch塊會使異常達不到應有的目的,即強迫你處理異常的情況。忽略異常就如同忽略
火警信號一樣——若把火警信號關掉瞭,當真正的火災發生時,就沒有人能看到火警信號
瞭。或許你會僥幸逃過一劫,或許結果將是災難性的。每當見到空的catch塊時,我們都應該
警鐘長鳴。
當然也有一種情況可以忽略異常,即關閉fileinputstream(讀寫本地文件)的時候。因為你還
沒有改變文件的狀態,因此不必執行任何恢復動作,並且已經從文件中讀取到所需要的信
息,因此不必終止正在進行的操作。
③finally:finally 代碼塊中的代碼用於處理善後工作, 會在最後執行,也一定會被執行。當遇
到try或catch中return或throw之類可以終止當前方法的代碼時,jvm會先去執行finally中的語
句,當finally中的語句執行完畢後才會返回來執行try/catch中的return,throw語句。如果
finally中有return或throw,那麼將執行這些語句,不會在執行try/catch中的return或throw語
句。finally塊中一般寫的是關閉資源之類的代碼。但是我們一般不在finally語句中加入return
語句,因為他會覆蓋掉try中執行的return語句。例如:
finally將最後try執行的return 10覆蓋瞭,最後結果返回瞭20.
④throws:在方法的簽名中,用於拋出此方法中的異常給調用者,調用者可以選擇捕獲或者
拋出,如果所有方法(包括main)都選擇拋出(或者沒有合適的處理異常的方式,即異常類
型不匹配)那麼最終將會拋給JVM,就會像我們之前沒使用try、catch語句一樣。JVM打印出
棧軌跡(異常鏈)。
⑤throw:用於拋出一個具體的異常對象。常用於自定義異常類中。
ps:
關於 "調用棧",方法之間是存在相互調用關系的, 這種調用關系我們可以用 "調用棧" 來描述.
在 JVM 中有一塊內存空間稱為 "虛擬機棧" 專門存儲方法之間的調用關系. 當代碼中出現異常
的時候, 我們就可以使用 e.printStackTrace() 的方式查看出現異常代碼的調用棧,一般寫在catch語句中。
異常處理流程
- 程序先執行 try 中的代碼
- 如果 try 中的代碼出現異常, 就會結束 try 中的代碼, 看和 catch 中的異常類型是否匹配.
- 如果找到匹配的異常類型, 就會執行 catch 中的代碼
- 如果沒有找到匹配的異常類型, 就會將異常向上傳遞到上層調用者.
- 無論是否找到匹配的異常類型, finally 中的代碼都會被執行到(在該方法結束之前執行).
- 如果上層調用者也沒有處理的瞭異常, 就繼續向上傳遞.
- 一直到 main 方法也沒有合適的代碼處理異常, 就會交給 JVM 來進行處理, 此時程序就會異常終止.
3.為什麼要使用異常?
存在即合理,舉個例子
//不使用異常 int[] arr = {1, 2, 3}; System.out.println("before"); System.out.println(arr[100]); System.out.println("after");
當我們不使用異常時,發現出現異常程序直接崩潰,後面的after也沒有打印。
//使用異常 int[] arr = {1, 2, 3}; try { System.out.println("before"); System.out.println(arr[100]); System.out.println("after"); } catch (ArrayIndexOutOfBoundsException e) { // 打印出現異常的調用棧 e.printStackTrace(); } System.out.println("after try catch");
當我們使用瞭異常,雖然after也沒有執行,但程序並沒有直接崩潰,後面的sout語句還是執行瞭
這不就是異常的作用所在嗎?
再舉個例子,當玩王者榮耀時,突然斷網,他不會讓你直接程序崩潰吧,而是給你斷線重連的機會吧:
我們再用偽代碼演示一把王者榮耀的對局過程:
不使用異常處理 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語句中加入信息提醒功能,比如你
開發瞭一個軟件,當那個軟件出現異常時,發個信息提醒你及時去修復。博主就做瞭一個小
小的qq郵箱信息提醒功能,源碼在碼雲,有興趣的可以去看看呀!需要配置qq郵箱pop3服
務,友友們可以去查查怎麼開啟呀,我們主旨不是這個所以不教怎麼開啟瞭。演示一下:
別群發消息哦,不然可能會被封號???
異常應隻用於異常的情況
try{ int i = 0; while(true) System.out.println(a[i++]); }catch(ArrayIndexOutOfBoundsException e){ }
這段代碼有什麼用?看起來根本不明顯,這正是它沒有真正被使用的原因。事實證明,作為
一個要對數組元素進行遍歷的實現方式,它的構想是非常拙劣的。當這個循環企圖訪問數組
邊界之外的第一個數組元素時,用拋出(throw)、捕獲(catch)、
忽略(ArrayIndexOutOfBoundsException)的手段來達到終止無限循環的目的。假定它與數
組循環是等價的,對於任何一個Java程序員來講,下面的標準模式一看就會明白:
for(int m : a) System.out.println(m);
為什麼優先異常的模式,而不是用行之有效標準模式呢?
可能是被誤導瞭,企圖利用異常機制提高性能,因為jvm每次訪問數組都需要判斷下標是否越
界,他們認為循環終止被隱藏瞭,但是在foreach循環中仍然可見,這無疑是多餘的,應該避
免。
上面想法有三個錯誤:
1.異常機制設計的初衷是用來處理不正常的情況,所以JVM很少對它們進行優化。
2.代碼放在try…catch中反而阻止jvm本身要執行的某些特定優化。
3.對數組進行遍歷的標準模式並不會導致冗餘的檢查。
這個例子的教訓很簡單:顧名思義,異常應隻用於異常的情況下,它們永遠不應該用於正常
的控制流。
總結:異常是為瞭在異常情況下使用而設計的,不要用於一般的控制語句。
4. 異常的種類
在Java中提供瞭三種可拋出結構:受查異常(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(); } }
我們發現test測試類中user使用方法報錯瞭,因為java認為checked異常都是可以再編譯階
段被處理的異常,所以它強制程序處理所有的checked異常,java程序必須顯式處checked
異常,如果程序沒有處理,則在編譯時會發生錯誤,無法通過編譯。
解決方案:
①try、catch包裹
IUser user = null; try { user.changePwd(); }catch (SafeException e){ e.printStackTrace(); } catch (RejectException e){ e.printStackTrace(); }
②拋出異常,將處理動作交給上級調用者,調用者在調用這個方法時還是要寫一遍try、catch
包裹語句的,所以這個其實是相當於聲明,讓調用者知道這個函數需要拋出異常
public static void main(String[] args) throws SafeException, RejectException { IUser user = null; user.changePwd(); }
4.2非受查異常
派生於error或runtime類的所有異常類就是非受查異常。
可以這麼說,我們現在寫程序遇到的異常大部分都是非受查異常,程序直接崩潰,後面的也
不執行。
像空指針異常、數組越界異常、算術異常等,都是非受查異常。由編譯器運行時給你檢查出
來的,所以也叫作運行時異常。
5.如何使用異常
避免不必要的使用受查異常
如果不能阻止異常條件的產生,並且一旦產生異常,程序員可以立即采取有用的動作,這種
受查異常才是可取的。否則,更適合用非受查異常。這種例子就是
CloneNotSuppportedException(受查異常)。它是被Object.clone拋出來的,Object.clone
隻有在實現瞭Cloneable的對象上才可以被調用。
被一個方法單獨拋出的受查異常,會給程序員帶來非常高的額外負擔,如果這個方法還有其
他的受查異常,那麼它被調用是一定已經出現在一個try塊中,所以這個異常隻需要另外一個
catch塊。但當隻拋出一個受查異常時,僅僅一個異常就會導致該方法不得不處於try塊中,也
就導致瞭使用這個方法的類都不得不使用try、catch語句,使代碼可讀性也變低瞭。
受查異常使接口聲明脆弱,比如一開始一個接口隻有一個聲明異常
interfaceUser{ //修改用戶名,拋出安全異常 publicvoid changePassword() throws MySecurityExcepiton; }
但隨著系統開發,實現接口的類越來越多,突然發現changePassword還需要拋出另一個異
常,那麼實現這個接口的所有類也都要追加對這個新異常的處理,這個工程量就很大瞭。
總結:如果不是非用不可,盡量使用非受查異常,或將受查異常轉為非受查異常。
6.自定義異常
我們用自定義異常來實現一個登錄報錯的小應用
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); } }
關於異常就到此為止瞭,怎麼感覺還有點意猶未盡呢?
到此這篇關於Java異常處理機制深入理解的文章就介紹到這瞭,更多相關Java 異常處理機制內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!