Java基礎之【深入講解Java異常】

這篇文章將詳細介紹 Java 的異常處理機制

異常介紹


☕️ 什麼是異常

程序運行時,發生的不被期望的事件,它阻止了程序按照程序員的預期正常執行,這就是異常。

☕️ Java異常的分類

Java 中的所有異常類都繼承於 Throwable 類,Throwable 主要包括兩個大類: Error 類和 Exception 類

  • 錯誤:Error 類及其子類的實例,代表了 JVM 本身的錯誤,包括虛擬機錯誤和線程死鎖,一旦 Error 出現了,程序就徹底的掛了,並且程序一般不會從錯誤中恢復,被稱爲程序終結者,例如:JVM 內存溢出。
  • 異常:Exception 類及其子類的實例,代表程序運行時發生的各種不期望發生的事件,可以被 Java 異常處理機制處理,是異常處理的核心。Exception 主要包括兩大類,非檢查異常(RuntimeException)和檢查異常(其他的一些異常)

在這裏插入圖片描述

非檢查異常(unckecked exception)

非檢查異常也叫 Runtime 異常(運行時異常),所有 RuntimeException 類及其子類的實例都被稱爲非檢查異常。javac 在編譯時,不會提示和發現這樣的異常,不要求在程序處理這些異常。對於這些異常,我們應該修正代碼,而不是去通過異常處理器處理,這樣的異常發生的原因多半是代碼寫的有問題,比如:除零異常 ArithmeticException、強制類型轉換異常 ClassCastException、數組索引越界異常 ArrayIndexOutOfBoundsException、使用了空對象異常 NullPointerException等等

檢查異常(checked exception)

檢查異常:不是 RuntimeException 類及其子類的異常實例被稱爲檢查異常。javac 強制要求程序員爲這樣的異常做預備處理工作(使用try…catch…finally或者throws),在方法中要麼用try-catch語句捕獲它並處理,要麼用throws子句聲明拋出它,否則編譯不會通過。這樣的異常一般是由程序的運行環境導致的。因爲程序可能被運行在各種未知的環境下,而程序員無法干預用戶如何使用他編寫的程序,於是程序員就應該爲這樣的異常時刻準備着。如SQLException , IOException,ClassNotFoundException 等。



捕獲異常


📝 異常捕獲的組成部分:

  • try塊:負責捕獲異常,一旦try中發現異常,程序的控制權將被移交給catch塊中的異常處理程序(try語句塊不可以獨立存在,必須與 catch 或者 finally 塊同存)
  • catch塊:如何處理?比如發出警告:提示、檢查配置、網絡連接,記錄錯誤等。執行完catch塊之後程序跳出catch塊,繼續執行後面的代碼(處理多個 catch 塊時,要按照先 catch 子類後 catch 父類的處理方式,因爲會就近處理/由上自下異常)
  • finally:最終執行的代碼,用於關閉和釋放資源。

Java 中,異常處理的任務就是將執行控制流從異常發生的地方轉移到能夠處理這種異常的地方去,也就是說:當一個函數的某條語句發生異常時,這條語句的後面的語句不會再執行,它失去了焦點

當執行流跳轉到最近的匹配的異常處理 catch 代碼塊去執行,異常被處理完後,執行流會接着在“處理了這個異常的catch代碼塊”後面接着執行,下面實例代碼展示了這種處理邏輯:

public static void main(String[] args){
    try {
        foo();
    }catch(ArithmeticException ae) {
        System.out.println("處理異常");
    }
}
public static void foo(){
    int a = 5 / 0;  					// 異常拋出點
    System.out.println("異常後面的代碼"); // 出現異常後面的代碼不會執行
}

📝 多重捕獲塊

一個 try 代碼塊後面跟隨多個 catch 代碼塊的情況就叫多重捕獲,如下面的代碼:

try {
    file = new FileInputStream(fileName);
    x = (byte) file.read();
} catch(FileNotFoundException f) { 
    f.printStackTrace();
    return -1;
} catch(IOException i) {
    i.printStackTrace();
    return -1;
} finally {
	// 處理最後的代碼
}

如果 try 塊代碼發生異常,異常被拋給第一個 catch 塊,如果異常匹配則執行第一個 catch 塊中的代碼,如果不匹配則傳遞給第二個 catch 塊進行異常匹配,以此類推,知道異常被捕獲或者通過所有的 catch 塊。多重異常處理代碼塊順序爲:先子類再父類(順序不對編譯器會提醒錯誤),finally 語句塊處理最終將要執行的代碼。



throw 和 throws 關鍵字


📖 throw 異常拋出語句

throw 的作用是:將產生的異常拋出,是手動拋出異常的一個動作。當手動拋出異常時,如果不使用 try catch 語句去嘗試捕獲這種異常,或者添加聲明來將異常拋出給更上一層的調用者進行處理的話,程序將會在這裏停止,並不會執行剩下的代碼。

一般會用於程序出現某種邏輯時程序員主動拋出某種特定類型的異常,如:

public static void main(String[] args) { 
    String s = "abc"; 
    if(s.equals("abc")) { 
      throw new NumberFormatException(); 
    } else { 
      System.out.println(s); 
    } 
}

>>>>>
Exception in thread "main" java.lang.NumberFormatException

📖 throws 函數聲明

throws 的作用是聲明將要拋出何種類型的異常。當某個方法可能會拋出某種異常時,用 throws 聲明可能拋出的異常,然後交給上層調用它的方法程序處理,如:

// 聲明可能會拋出 NumberFormatException 異常
public static void function() throws NumberFormatException{ 
    String s = "abc"; 
    System.out.println(Double.parseDouble(s)); 
} 
    
public static void main(String[] args) { 
    try { 
      function(); 
    } catch (NumberFormatException e) { 	// 捕獲 function() 可能拋出的異常
      System.err.println("非數據類型不能轉換。");  
    } 
}

📖 throw 與 throws 的比較

  • throw 出現在函數體,而 throws 出現在方法函數頭
  • throw 是直接拋出異常,執行 throw 則一定發生了某種異常,而 throws 表示出現異常的一種可能性,並不一定會發生這些異常
  • 兩者都是消極處理異常的方式(這裏的消極並不是說這種方式不好),只是拋出或者可能拋出異常,但是不會由函數去處理異常,真正的處理異常由函數的上層調用處理。
注意:如果某個方法調用了拋出異常的方法,那麼必須添加 try catch 語句去嘗試捕獲這種異常, 或者添加聲明,將異常拋出給更上一層的調用者進行處理



finally 塊和 return


首先我們需要知道:在 try 塊中即便有 return,break,continue 等改變執行流的語句,finally 塊代碼也會執行,比如:

public static void main(String[] args)
{
    try{
        return 5;
    } finally{
        System.out.println("執行 finally");
    }
}

>>>>>
執行 finally

🥚 finally 中的 return 會覆蓋 try 或者 catch 中的返回值

public static void main(String[] args) {
    int re1 = testException1();
    System.out.println(re1);

    int re2 = testException2();
    System.out.println(re2);
}

public static int testException1() {
    try {
        return 1;
    } finally {
        return 2;
    }
}

public static int testException2() {
    try {
        int a = 5 / 0;
    } catch (ArithmeticException e) {
        return 2;
    } finally {
        return 3;
    }
}

>>>>>
2
3

🥚 finally 中的 return 會抑制前面 try 或者 catch 塊中的異常

public class TestException {
    public static void main(String[] args){
        try{
            System.out.println(foo());           // 輸出100
        } catch (Exception e){
            System.out.println(e.getMessage());  // 沒有捕獲到異常
        }

        try{
            System.out.println(bar());           // 輸出100
        } catch (Exception e){
            System.out.println(e.getMessage());  // 沒有捕獲到異常
        }
    }

    // catch 中的異常被抑制
    public static int foo() throws Exception{
        try {
            int a = 5 / 0;
            return 1;
        }catch(ArithmeticException amExp) {
            throw new Exception("我將被忽略,因爲下面的finally中使用了return");
        }finally {
            return 100;
        }
    }

    // try 中的異常被抑制
    public static int bar() throws Exception{
        try {
            int a = 5 / 0;
            return 1;
        }finally {
            return 100;
        }
    }
}

🥚 finally 中的異常會覆蓋前面 try 或者 catch 中的異常

class TestException {
    public static void main(String[] args){
        try{
            foo();
        } catch (Exception e){
            System.out.println(e.getMessage());    // 輸出:我是finaly中的Exception
        }
 
        try{
            bar();
        } catch (Exception e){
            System.out.println(e.getMessage());    // 輸出:我是finaly中的Exception
        }
    }
 
    // catch 中的異常被抑制
    public static void foo() throws Exception{
        try {
            int a = 5 / 0;
        }catch(ArithmeticException amExp) {
            throw new Exception("我將被忽略,因爲下面的finally中拋出了新的異常");
        }finally {
            throw new Exception("我是finaly中的Exception");
        }
    }
 
    // try 中的異常被抑制
    public static void bar() throws Exception{
        try {
            int a = 5 / 0;
        }finally {
            throw new Exception("我是finaly中的Exception");
        }
    }
}

這三種情況都是不好的編程習慣,所以我們建議:

  • 不要在 fianlly 中使用 return
  • 不要在 finally 中拋出異常
  • 減輕 finally 的任務,不要在 finally 中做一些其它的事情,finally 塊僅僅用來釋放資源是最合適的
  • 儘量將所有的 return 寫在函數的最後面,而不是try … catch … finally 中



自定義異常


📖 爲什麼要使用自定義異常

  • 我們在工作的時候,項目是分模塊或者分功能開發的,基本不會你一個人開發一整個項目,使用自定義異常類就統一了異常對外展示的方式
  • 自定義異常可以在我們項目中處理某些特殊的業務邏輯時拋出異常,而這種異常是 Java 沒有的,換句話說,系統中有些錯誤是符合 Java 語法的,但不符合我們項目的業務邏輯
  • 有時候我們遇到某些校驗或者問題時,需要直接結束掉當前的請求,這時便可以通過拋出自定義異常來結束,如果你項目中使用了 SpringMVC 比較新的版本的話會有控制器增強,可以通過 @ControllerAdvice 註解寫一個控制器增強類來攔截自定義的異常並響應給前端相應的信息

📖 自定義異常的方式

自定義異常需要注意以下幾點:

  • 所有異常都必須是 Throwable 的子類
  • 如果希望寫一個檢查性異常類,則需要繼承 Exception 類
  • 如果你想寫一個運行時異常類,那麼需要繼承 RuntimeException 類

下面我看一個自定義異常例子:

// 自定義一個檢查性異常類
public class MyException extends Exception {
    // 錯誤編碼
    private String errorCode;
 
    public MyException(String message){
        super(message);
    }

    public String getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(String errorCode) {
        this.errorCode = errorCode;
    }

    public static void main(String[] args) {
    	try {
    		throw new MyException("拋出自定義異常");
    	} catch(MyException e) {
    		e.printStackTrace();
    	}
    }
}

>>>>>
com.my.main.MyException: 拋出自定義異常
	at com.my.main.MyException.main(MyException.java:23)

自定義異常就是這麼簡單,可以根據實際業務需求去拋出相應的自定義異常。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章