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!

推薦閱讀: