異常層次結構及異常處理(try、catch、finally)
在Java程式語言設計中,所有的異常都是由類來表示,異常物件都是派生於Throwable類的一個例項。下面是Java異常層析結構的一個簡單示意圖:
由圖可知,所有異常類都直接或間接的繼承於Throwable類。
具體分類:
Java把所有非正常情況分為Error(錯誤) 和 Exception(異常)
Error
Error類層次結構描述了Java執行時系統的內部錯誤和資源耗盡錯誤,應用程式不應該丟擲此型別的物件。這種情況一般很少出現。
- VirtualMachineError(虛擬機器錯誤)
- ThreadDeath(執行緒鎖死)
Exception
Exception類層次結構又分為兩個分支 RuntimeException(執行時異常)和其他異常(代表:IOException )。
派生於RuntimeException的常見異常的有以下幾個
- ClassCastException(型別轉換異常)
- IndexOutOfBoundsException(下標越界異常)
- NullPointerException(空指標異常)
若是出現RuntimeException異常,那麼一就定是你的問題,這是一條相當由道理的規則。
不派生於RuntimeException的異常有
- IOException(I /O流異常)
- 試圖在檔案尾部後面讀取資料
- 試圖開啟一個不存在的檔案
- 試圖根據指定的字串查詢Class物件,而這個字串表示的物件不存在。
Java語言規範將派生於Error類 或RuntimeException類的所有異常成為非受察(unchecked)異常,所有其他異常稱為受察(checked)異常。
異常處理機制
當程式出現異常時無非是將可能出現的異常丟擲去不予理睬或者捕獲可能出現的異常並加以處理。
丟擲:
丟擲異常依賴於throw和throws關鍵字
throw:丟擲一個具體的異常物件,可以單獨作為語句使用,可以在符合某種條件下丟擲異常。
throws:用在方法簽名(方法名和引數列表統稱為方法簽名)中,宣告該方法可能丟擲的異常(類)。
例1:
/** * @author 北冥有熊 * 2018年11月3日 */ public class ecxeptionTest { public static void main(String[] args) { try { mathException(10,0); } catch (Exception e) { System.out.println(e.getMessage()); //返回throwable詳細資訊字串 e.printStackTrace(); //將throwable物件的堆疊跟蹤輸出至錯誤輸出流。 } } public static void mathException(int a,int b) throws Exception{ //宣告有可能丟擲一個異常 if (b==0) { //在此方法裡面並沒有對可能出現的異常進行處理,而是產生一個具體的異常物件並丟擲給呼叫者 throw new Exception("除數不能為0"); } else { int num = a/b; System.out.println(a+"/"+b+"=:"+num); } } }
其中,宣告異常部分也可以宣告多個異常,之間用 “,”隔開
public static void mathException(int a,int b) throws Exception,OtherException{}
捕獲並處理:
捕獲處理異常依賴於try、catch、finally等關鍵字
try:存放需要捕獲指定異常的語句塊。
catch:捕獲異常物件,並加以處理,chtch語句可以定義多個,針對不同異常如果有不同的處理手段。
finally:在其裡面的程式碼塊必須執行
/**
* @author 北冥有熊
* 2018年11月4日
*/
public class exceptionTest01 {
public static void main(String[] args) {
try {
int a = 10;
int num = a/0; //發現異常,停止執行,轉向catch語句塊
System.out.println("-------1-------"); //不執行
} catch (Exception e) {
System.out.println("-------2-------"); //執行
}finally {
System.out.println("-------3-------"); //必須執行
}
}
}
結果是:
-------2-------
-------3-------
值得注意的是,當try、catch語句塊中有return時有如下特點:
/**
* @author 北冥有熊
* 2018年11月4日
*/
public class Test03 {
public static void main(String[] args) {
System.out.println(new Demo().getNum1());
System.out.println(new Demo().getNum2());
}
}
class Demo{
//finally中沒有return
String getNum1(){
String t="";
try{
t="try";
return t; //此處返回的t並非上一句中的t,而是系統重新指定區域性引用的t'
//只不過t'指向了引用t對應的值,也就是try
}catch(Exception e){
t="catch"; //不執行
return t;
}finally{
t="finally";
System.out.println("===="+t); // 輸出t=finally,此處修改的是t而非t',
//t'雖然指向t但返回的是未修改的t'-----指向----->t="try"
}
}
//fianlly中有return
String getNum2(){
String t="";
try{
t="try";
return t; //方法結束,去檢查finally語句塊中有沒有return語句
}catch(Exception e){
t="catch"; //不執行
}finally{
t="finally";
System.out.println("===="+t); // 輸出t=finally
return t; // 返回修改後的t'指向上一句被修改過得t,並且返回[finally中一般不寫return
//此處警告,但能執行
}
}
}
getNum1方法結果:【finally中沒有return】
====finally //t'-----指向----->t="try"被修改為finally
try //在未修改t之前已經返回
getNum2方法結果:【finally中有return】
====finally //t被修改為finally
finally //修改之後並覆蓋前面的return返回值
對於上面程式碼的註解大家可能很疑惑,下面看用javap -verbose Test03來顯示目標檔案(.class檔案)位元組碼資訊:
這裡我們只看有效方法主體;
public static java.lang.String main();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=0
0: ldc #5 // String
2: astore_0
3: ldc #6 // String try
5: astore_0
6: aload_0
7: astore_1
8: ldc #7 // String finally
10: astore_0
11: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
14: new #8 // class java/lang/StringBuilder
17: dup
18: invokespecial #9 // Method java/lang/StringBuilder."<init>":()V
21: ldc #10 // String ====
23: invokevirtual #11 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
26: aload_0
27: invokevirtual #11 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
30: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
33: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
36: aload_1
37: areturn
38: astore_1
...
88: ldc #10 // String ====
90: invokevirtual #11 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
93: aload_0
94: invokevirtual #11 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
97: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
100: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
103: aload_3
104: athrow
Exception table:
from to target type
3 8 38 Class java/lang/Exception
3 8 74 any
38 44 74 any
LocalVariableTable:
Start Length Slot Name Signature
39 35 1 e Ljava/lang/Exception;
3 102 0 t Ljava/lang/String;
}
首先看LocalVariableTable資訊,這裡面定義了兩個變數 一個是e Exception 型別,一個是t String型別
接下來看方法主體Code部分:
- 第[0-2]行:從常量池入口#5取出內容推到棧頂,將"null"引用傳遞給給第0個變數,也就是String t = "";
- 第[3-6]行:從常量池入口#6取出內容推到棧頂,也就是執行try語句塊,將"try"引用傳遞給第0個變數。
- 第[7]行:重點是第7行,將"try"付給第1個變數(astore_1),但是這裡面第1個變數並沒有定義,(納悶中...?)
- 第[8-10] 行:對第0個變數進行賦值操作,也就是t="finally"。
- 第[36-37]行:把第1個變數對應的值返回
通過位元組碼,我們發現,在try語句的return塊中,return 返回的引用變數(t 是引用型別)並不是try語句外定義的引用變數t,而是系統重新定義了一個區域性引用t’,這個引用指向了引用t對應的值,也就是try ,即使在finally語句中把引用t指向了值finally,因為return的返回引用已經不是t ,所以引用t的對應的值和try語句中的返回值無關了。
總結:
- finally語句塊中的程式碼是必須執行的,當在try語句塊總中出現異常,程式碼停止當前執行,轉向catch語句塊,再轉向finally語句塊。
- 在try、catch、finally中,若try、catch中有return時,且執行到return時,首先會檢查finally語句塊中有沒有return,若finally中沒有就返回try、catch語句塊中的return返回值;若finally語句塊中有return,則finally中的return值會覆蓋try、catch中的return返回值。
- 若try、catch中有異常資訊時,finally塊中應避免使用return語句,因為finally塊中如果使用return語句,會顯示的遮蔽掉try、catch塊中的異常資訊,這並不是我們想要的。