學習筆記(五):Java異常機制
主要從這幾個方面來說說Java中的異常:
1. 異常:指的是程式在執行過程中,出現的非正常的情況,最終會導致JVM的非正常停止。
異常的繼承體系如下:
Throwable類是 Java 語言中所有錯誤或異常的父類。所以,如果要實現自定義異常,那麼就要繼承這個類或其子類。
Error:不做過多陳述,出現錯誤是非常嚴重的,因為Error是無法處理(虛擬機器相關的問題:系統崩潰,虛擬機器錯誤,記憶體空間不足,方法呼叫棧溢)必須找到導致錯誤的根源,修改程式碼。
Exception:異常,也就是平時所說的“報錯”,具體表現就是IDE控制檯出現紅色的資訊,異常類以......Exception結尾,他有非常多的子類。
如圖,我們都知道除數不能為0,當除數為0時,就會有一個算術異常(java.lang.ArithmeticException: / by zero):
異常的分類:如圖2所示,在我們編寫程式碼時,並沒有出現任何錯誤提示,而當啟動程式執行時,出現了這個異常。
我們把這個叫做:執行時異常(runtimeException) ;當寫程式碼的過程中出現錯誤提示(紅色波浪線),稱為:編譯錯誤。
常見的執行異常有1. NullPointerExceptin 空指標 2. IndexOutOfBoundsException 索引越界 3. ClassCastException 類轉換 4. IllegalArgumentException 非法引數
問題:異常是怎麼產生的(異常產生的過程)?
簡單說一下,異常產生的過程。我們都知道,java程式都是經過編譯後,執行在JVM(Java虛擬機器)上,如圖2,程式碼經過編譯為.class檔案,JVM載入.class檔案開始執行:①JVM檢測到程式做1除以0運算,但除數不能為0,於是,JVM建立了異常物件:exception = new ArithmeticException(); ②丟擲異常物件(throw exception); ③把異常傳遞給方法呼叫者(這裡為main),但main並沒有處理這個異常,又傳給了JVM;④JVM處理異常簡單粗暴:列印異常資訊,然後終止程式執行。
問題:以圖2為例,JVM處理異常簡單粗暴,那麼我們能否在JVM處理前,自己把這個異常丟擲?
當然可以,我們可以在做運算前檢查一下引數,這裡主要檢查引數b。如果有問題自己在方法內丟擲異常。程式碼如下:
public static void main(String[] args) {
int a = 1,b = 0;
getResult(a, b);
}
//計算a除以b的結果的方法
private static void getResult(int a, int b) {
//檢查引數
if (b == 0){
//仿照JVM內,丟擲(throw)一個(宣告好的)算數異常
throw new ArithmeticException("b 不能為0");
}
int i = a/b;
System.out.println(i);
}
2. 那麼,該如何在出現異常時優雅的處理異常,讓程式不停止執行或者奔潰?
處理異常,離不開五個關鍵字:try、catch、finally、throw、throws。
①先說第一個關鍵字,throw(丟擲):這個關鍵字,在上述示例中已經出現。有關throw關鍵字的使用:throw是使用在方法內,將這個異常傳遞(動態的丟擲)到物件呼叫者處,並停止當前方法的執行(後面的方法也不會執行)。
格式:throw new 異常類名(引數)。 如:throw new NullPointerException("要訪問的值不存在");
作用: 警告方法呼叫者,非法呼叫當前方法。
原始碼解讀_Objects.requireNonNull()方法: 這是java.util.Objects工具類的一個檢視指定引用物件不為空的靜態方法,該方法主要用於驗引數驗證和建構函式。
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
如果這個方法用於圖2的引數驗證和我們自己丟擲異常效果是一樣的,只不過直接使用,更方便。
②第二個關鍵字,throws(丟擲):這裡準確說為宣告異常,即,將可能會出現的問題標識出來,報告給方法呼叫者,由呼叫者處理。【如果說異常是一個“流氓”,那麼throws的處理方式為,我打不過你,我去叫人處理你;而稍後要說的try...catch,則一點“不慫”,直接踹你,把你KO了】
用法:運用於方法宣告之上,用於表示當前方法不處理異常,而是提醒該方法的呼叫者來處理異常(丟擲異常).
格式:方法名(引數)throws 異常類名1,異常類名2......{//方法體}
示例稍後展示;
③try...catch...(catch...)finally:
如果發生異常,不處理的話,程式就會立刻停止,這樣帶來的後果是很嚴重的,所以,我們要處理異常,即,用try..catch...
try中寫的是可能會出現異常的程式碼,catch來捕獲異常,當try中的程式碼出現問題時,catch就會立刻做出響應,捕獲並處理異常;反之,try中的程式碼正常執行,也就沒catch什麼事了。
語法 (try和catch都不能單獨使用,必須連用):
try{ //編寫可能會出現異常的程式碼 }catch(異常型別 e){ //處理異常的程式碼 //記錄日誌/列印異常資訊/繼續丟擲異常 }
public static void main(String[] args) {
method();
System.out.println("程式執行結束了");
}
private static void method() {
int[] array = {1,2,3};
try{
int e = array[3]; //JVM : throw new ArrayIndexOutOfBoundsException()
System.out.println(e);
}catch (Exception e){ // e = new ArrayIndexOutOfBoundsException()
// 發生異常並捕獲了,執行這裡
System.out.println("呵呵,異常發生了");
}
}
這樣雖然捕獲了異常,並且不影響後面的程式碼執行,但是,如果業務邏輯複雜,要想快速定位到發生異常的原因,位置,就要呼叫printStackTrace()方法,這個方法的作用是列印棧中追溯,簡單說,就是列印異常資訊(包含了異常的型別,原因和出現的位置)。只需要在catch中加入:
// 列印棧中追溯 (列印異常資訊)
e.printStackTrace();
那麼上面說的throws,將異常宣告,由方法呼叫者處理,那麼,方法呼叫者就需要try...catch來處理。
例:
public static void main(String[] args) {
String date = "2018-09-09"; //定義個時間,這裡格式是正確的,可以試試錯的
//呼叫方法,需要處理這個方法丟擲的異常
try {
method(date);
} catch (ParseException e) {
e.printStackTrace();
}
}
//將字串型別時間轉換成Date型別
private static void method(String date) throws ParseException {
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
//parse 方法可能有一個格式轉換的問題,也就是引數格式個定義的轉換格式不一致
//這裡不處理,而是丟擲
Date date1 = dateFormat.parse(date);
System.out.println(date1);
}
那麼當,try中的程式碼可能發生多種異常時,那麼可能就需要“分類討論”,也就是分別列出可能發生異常情況;
語法:
try{ //編寫可能會出現異常的程式碼 }catch(異常型別 e){ //處理異常的程式碼 //記錄日誌/列印異常資訊/繼續丟擲異常 } catch (異常型別 e){
}
//以下程式碼只是示例,不具有任何實際意義
public static void main(String[] args) {
try {
method(1);
} catch (NullPointerException e) {
// 如果出現空指標異常
e.printStackTrace();
System.out.println(0);
}catch (IndexOutOfBoundsException e) {
//如果出現越界異常
e.printStackTrace();
System.out.println(1);
}
System.out.println("程式繼續執行");
}
private static void method(int i) {
if(i == 0){
throw new NullPointerException("空指標異常");
}else{
throw new ArrayIndexOutOfBoundsException("越界異常了");
}
}
我們都知道Exception是所有異常的父類,可不可以直接用一個catch(Exception e)處理呢?當然是可以的。那麼,這樣“分類討論”有什麼意義呢?
當Java執行時,出現異常,就需要處理,而異常種類又是多種多樣的,不同的異常,根據實際業務需求處理的方式可能不同,所以,需要不同的catch塊來處理專門的異常。【多個catch塊處理時,需要,注意異常等級需要從小到大,也就是第一個catch處理的異常一定不能是他的父類,因為父類異常一定能處理子類的異常,要是父類異常都處理完了,要子類異常來幹嘛!】
最後來說說,finally程式碼塊,finally意為最後的,在這裡的意思就是,無論這個這個程式碼try了,還是catch了,finally都要執行。
public static void main(String[] args) {
try{
//可能出現異常的程式碼
int i = 1/0;
System.out.println(i);
}catch(NullPointerException e){
// 抓到異常,會執行這裡
System.out.println("抓住了");
}finally {
// 無論如何都執行
System.out.println("無論如何都執行");
}
System.out.println("程式繼續執行");
}