Java 異常

在程序執行過程中,可能有各種出錯的情況,有的是不可控的內部原因,比如內存不夠了、磁盤滿了,有的是不可控的外部原因,比如網絡連接有問題,更多的可能是程序的編寫錯誤,比如引用變量未初始化就直接調用實例方法。這些非正常情況在Java中都被認為是異常。

異常簡介

異常是相對于 return 的一種退出機制,可以由系統觸發,也可以由程序通過 throw 語句觸發,異常可以通過 try/catch 語句進行捕獲并處理,如果沒有捕獲,則會導致程序退出并輸出異常棧信息。

當發生異常時,Java 會啟用異常處理機制,首先創建一個異常對象,然后異常處理機制會從當前方法開始查找看誰 “捕獲” 了這個異常,當前方法沒有就查看上一層,直到 main 方法,如果也沒有,就使用默認機制,輸出異常棧信息并退出。

異常棧信息包括從異常發生點到最上層調用者的軌跡,還包括行號,是分析異常最為重要的信息。

對于異常棧信息,普通用戶無法理解,也不知道該怎么辦,我們需要給用戶一個更為友好的信息,又或者我們不希望程序在出現異常后就退出程序,而是拋出異常然后繼續執行,為此需要使用 try/catch 捕獲異常。

try 后面的花括號 {} 內包含可能拋出異常的代碼,catch 語句包含能捕獲的異常和處理代碼,捕獲異常后,程序就不會異常退出了,但 try 語句內異常點之后的代碼不會執行,執行完 catch 內的語句后,程序會繼續執行 try/catch 之后的代碼。

public class TestUtil {

    public static void main(String[] args) {
        excepTest1();
        excepTest2();
        System.out.println("main 方法沒有捕獲異常,不會繼續執行");
    }

    public static void excepTest1(){
        /**
         * 發生異常時,會啟用異常處理機制,首先創建一個異常對象
         * 異常處理機制會從當前方法開始查找看誰 “捕獲” 了這個異常,
         * 當前方法沒有就查看上一層,直到 main 方法,
         * 如果也沒有,就使用默認機制,輸出異常棧信息并退出
         */
        String str = "abc";
        try {
            int num = Integer.parseInt(str);
            System.out.println("如果異常,try 語句內,異常點后的代碼不會執行");
            System.out.println(num);
        } catch (NumberFormatException e1){
            System.err.println("參數:"+ str +",不是有效數字");
            e1.getMessage();
            e1.getCause();
            e1.printStackTrace();
        } catch (Exception e2){
            /**
             * 異常處理機制將根據拋出的異常類型找第一個匹配的 catch 塊,
             * (Java 7 開始,多個異常之間可以用“|”操作符,但注意異常之間不能有父子關系)
             * 找到后,執行catch塊內的代碼,不再執行其他 catch 塊,
             * 如果沒有找到,會繼續到上層方法中查找。
             * 需要注意的是,如果拋出的異常類型是 catch 中聲明異常的子類也算匹配(多態)
             */
            System.err.println("異常類型不是 NumberFormatException 才會執行");
            throw e2;
        } finally {
            System.out.println("不管有沒有異常,一定執行");
        }
        System.out.println("當前有捕獲異常,方法會繼續執行");
    }

    public static void excepTest2(){
        String str = "abc";

        int num = Integer.parseInt(str);
        System.out.println("當前沒有捕獲異常,方法不會繼續執行,向上級方法查找異常捕獲");
        System.out.println(num);
    }

    /**
     * throws 跟在方法的括號后面,可以聲明多個異常,以逗號分隔
     * 主要用于在父類方法中聲明可能(包括子類)拋出的異常
     * @throws Exception
     */
    public static void excepTest3() throws Exception {
        try {

        } catch (Exception e){
            e = new Exception(); // 注釋該行,可以去掉方法后的 throws
            throw e;
        }
    }
}

異常體系

[圖片上傳失敗...(image-3fc672-1677849073641)]

Throwable

java.lang.Throwable 是所有異常的基類,它有兩個子類:Error 和 Exception。

Error 用來指示運行時環境發生的錯誤,一般發生在嚴重故障時,Java 程序通常不捕獲錯誤,比如 內存溢出錯誤(OutOfMemory-Error)和棧溢出錯誤(StackOverflowError)。

Exception 表示應用程序錯誤,它有很多子類,應用程序也可以通過繼承 Exception 或其子類創建自定義異常。

RuntimeException 是未受檢異常(unchecked exception),IOException、SQLException 和 Exception 自身則是受檢異常(checked exception)。對于受檢異常,Java 會強制要求程序員進行處理,否則會有編譯錯誤,而對于未受檢異常則沒有這個要求。

大部分類在繼承父類后只是定義了幾個構造方法,這些構造方法也只是調用了父類的構造方法,并沒有額外的操作。定義這么多不同的異常類主要是為了名字不同,異常類的名字本身就代表了異常的關鍵信息。

  • 構造方法
public Throwable()
public Throwable(String message)
public Throwable(String message, Throwable cause)
public Throwable(Throwable cause)

message,表示異常消息;cause,表示觸發該異常的其他異常。

異常可以形成一個異常鏈,上層的異常由底層異常觸發,cause表示底層異常。

  • 設置 cause

這個方法最多只能被調用一次

Throwable initCause(Throwable cause)
  • 保存異常棧信息

用當前的調用棧層次填充 Throwable 對象棧層次,添加到棧層次信息中。

public Throwable fillInStackTrace()
  • 獲取異常信息
public String getMessage()
  
public Throwable getCause()
  • 打印異常棧信息
public void printStackTrace()

自定義異常

一般是繼承 Exception 或者它的某個子類。如果父類是 RuntimeException 或它的某個子類,則自定義異常也是未受檢異常;如果是 Exception 或 RuntimeException 以外的其他子類,則自定義異常是受檢異常。

// 自定義異常類,繼承Exception類
public class InsufficientFundsException extends Exception {
  // 此處的amount用來儲存當出現異常(取出錢多于余額時)所缺乏的錢
  private double amount;
  
  public InsufficientFundsException(double amount) {
    this.amount = amount;
  } 
  
  public double getAmount() {
    return amount;
  }
}

異常處理

try catch

使用 try 和 catch 關鍵字可以捕獲異常。try/catch 代碼塊放在異常可能發生的地方,try/catch 中的代碼稱為保護代碼。

// 自定義異常類,繼承Exception類
public class InsufficientFundsException extends Exception {
  // 此處的amount用來儲存當出現異常(取出錢多于余額時)所缺乏的錢
  private double amount;
  public InsufficientFundsException(double amount) {
    this.amount = amount;
  } 
  public double getAmount() {
    return amount;
  }
}

一個 try 代碼塊后面跟隨多個 catch 代碼塊的情況就叫多重捕獲。

try {
    file = new FileInputStream(fileName);
    x = (byte) file.read();
} catch(FileNotFoundException f) { // Not valid!
    f.printStackTrace();
    return -1;
} catch(IOException i) {
    i.printStackTrace();
    return -1;
}

如果保護代碼中發生異常,異常被拋給第一個 catch 塊。如果不匹配,它會被傳遞給第二個 catch 塊,直到異常被捕獲或者通過所有的 catch 塊。

需要注意的是,拋出的異常類型是 catch 中聲明異常的子類也算匹配,所以需要將最具體的子類放在前面。

這種寫法比較煩瑣,Java 7開始支持一種新的語法,多個異常之間可以用 “|” 操作符。

try {
  //可能拋出 ExceptionA和ExceptionB
} catch (ExceptionA | ExceptionB e) {
  e.printStackTrace();
}

throw

在 catch 塊內處理完后,可以重新拋出異常,異常可以是原來的,也可以是新建的。

如果一個方法拋出檢查性異常,那么該方法必須使用 throws 關鍵字來聲明拋出的異常,否則不能拋出。

throws 跟在方法的括號后面,可以聲明多個異常,以逗號分隔。

聲明的含義是,這個方法內可能拋出這些異常,且沒有沒有處理完這些異常,調用者必須進行處理。

public void test() throws Exception {
  try{
    // 可能觸發異常的代碼
  }catch(NumberFormatException e){
    System.out.println("not valid number");
    throw new AppException("輸入格式不正確", e);
  }catch(Exception e){
    e.printStackTrace();
    throw e;
  }
}

finally

finally 內的代碼不管有無異常發生,都會執行。一般用于釋放資源,如數據庫連接、文件流等。

  • 如果沒有異常發生,在 try 內的代碼執行結束后執行
  • 如果有異常發生且被 catch 捕獲,在 catch 內的代碼執行結束后執行
  • 如果有異常發生但沒被 catch 捕獲,則在異常 throw 給上層之前執行
  • 如果在 try 或者 catch 語句內有 return 語句,則 return 語句在 finally 語句執行結束后才執行,對于基本數據類型,finally 中并不能改變返回值(可以理解為返回值存儲在返回值存儲器中,finally 的操作不會改變返回值存儲器中的值),對于對象類型,可以改變其中的值(返回值存儲器存儲的是對象地址)。
  • 如果 finally 中有 return,不僅會覆蓋 try 和 catch 內的返回值,還會掩蓋 try 和 catch 內的異常,就像異常沒有發生一樣,應該避免在 finally 中使用 return 語句或者拋出異常
try {
  // 可能拋出異常
} catch (Exception e){
  // 捕獲異常
} finally {
  // 不管有無異常都執行
}

try-with-resources

對于一些使用資源的場景,比如文件和數據庫連接,典型的使用流程是首先打開資源,最后在 finally 語句中關閉資源。

Java 7 開始支持一種新的語法 try-with-resources,針對實現了 java.lang.AutoCloseable 接口的對象。

資源的聲明和初始化放在 try 語句內,不用再調用 finally,在語句執行完 try 語句后,會自動調用資源的 close() 方法。

try-with-resources 語句中可以聲明多個資源,使用分號 ; 分隔各個資源。

Java 9 之前,資源必須聲明和初始化在try語句塊內,Java 9 去除了這個限制,資源可以在 try 語句外被聲明和初始化,但必須是 final 的或者是事實上 final 的(即雖然沒有聲明為 final 但也沒有被重新賦值)。

try(AutoCloseable autoClose = new FileInputStream("hello")){
  //
}

異常處理的目標

異常大概可以分為三種來源:用戶、程序員、第三方。

用戶是指用戶的輸入有問題;程序員是指編程錯誤;第三方泛指其他情況,如 I/O 錯誤、網絡、數據庫、第三方服務等。

處理的目標可以分為恢復和報告。恢復是指通過程序自動解決問題。報告的最終對象可能是用戶,即程序使用者,也可能是系統運維人員或程序員。報告的目的也是為了恢復,但這個恢復經常需要人的參與。

對用戶,如果用戶輸入不對,可以提示用戶具體哪里輸入不對,如果是編程錯誤,可以提示用戶系統錯誤、建議聯系客服,如果是第三方連接問題,可以提示用戶稍后重試。

異常處理的一般邏輯

如果自己知道怎么處理異常,就進行處理;如果可以通過程序自動解決,就自動解決;如果異常可以被自己解決,就不需要再向上報告。

如果自己不能完全解決,就應該向上報告。如果自己有額外信息可以提供,有助于分析和解決問題,就應該提供,可以以原異常為 cause 重新拋出一個異常。

總有一層代碼需要為異常負責,可能是知道如何處理該異常的代碼,可能是面對用戶的代碼,也可能是主程序。如果異常不能自動解決,對于用戶,應該根據異常信息提供用戶能理解和對用戶有幫助的信息;對運維和開發人員,則應該輸出詳細的異常鏈和異常棧到日志。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容