在程序執行過程中,可能有各種出錯的情況,有的是不可控的內部原因,比如內存不夠了、磁盤滿了,有的是不可控的外部原因,比如網絡連接有問題,更多的可能是程序的編寫錯誤,比如引用變量未初始化就直接調用實例方法。這些非正常情況在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 重新拋出一個異常。
總有一層代碼需要為異常負責,可能是知道如何處理該異常的代碼,可能是面對用戶的代碼,也可能是主程序。如果異常不能自動解決,對于用戶,應該根據異常信息提供用戶能理解和對用戶有幫助的信息;對運維和開發人員,則應該輸出詳細的異常鏈和異常棧到日志。