Java——你真的瞭解Java例外處理機制嗎?

2022-01-04 08:00:02

目錄

1.初識異常

 2.異常的基本用法

例外處理流程

 3.為什麼要使用異常?

異常應只用於異常的情況

4. 異常的種類

 4.1 受查異常

解決方案:

4.2非受查異常

5.如何使用異常

避免不必要的使用受查異常

6.自定義異常


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);
    }
}

 

 

關於異常就到此為止了,怎麼感覺還有點意猶未盡呢?