Java 異常你必須瞭解的一些知識

極速24號發表於2019-02-14
1.概述

程式執行時的錯誤主要分兩種,第一種是語法錯誤,第二種是語意錯誤

通常情況下,開發工具會幫開發者診斷出語法錯誤,例如大家喜聞樂見的空指標異常,但語意錯誤開發工具卻不那麼容易幫開發者檢測出來了,因為沒有一個明確的標準,開發工具不知道到底怎樣算對、怎樣算錯。用一位國際友人的話說就是:

  • If you make an error with the syntax, you'll have the compiler complain about it.
  • If you make an error with the semantics, you'll have people complain about it (especially your boss).
1.1 語法 & 語意

語法:在程式語言中,語法是定義符號組合的規則,它使這些符號以正確的結構或片段展示。

如:He is a man.(主、謂、賓)

語意:在程式語言理論中,語意是程式碼的含義,它描述了計算機用特定語言執行程式時所遵循的過程。

如:He is a man.(他是一個男人)

由上我們就可以知道,語法和語意只不過是站在不同的角度看待同一事物而已。

大致搞明白語法和語意的概念之後,大家自然就明白為什麼開發工具可以輕易地幫我們檢測出語法錯誤,但很難幫我們發現語意錯誤了——因為語法是死的,該什麼樣就什麼樣,不需要你來創造任何東西,只要按照規定的格式往裡面填符號就好了,正如上面的例子,如果你把謂語和賓語的位置顛倒一下——He a man is,開發工具立馬就可以檢測出問題。但如果你將上面的句子改成——Grass eat sun,雖然這句話毫無意義,但開發工具是不知道的,因為它完全符合語法規則。因此出現語意錯誤時,你唯一可能惹怒的只有——要麼是你的隊友,要麼是你的老闆。

2. 異常基本概念及繼承結構
2.1 什麼是異常?

異常:導致程式中斷執行的一種指令流。

我們必須明確一點:即使程式異常了,它也是按照某種邏輯在執行,只是沒有按照我們給它安排的邏輯執行。 但錯歸錯,錯並不代表沒有邏輯,任何程式碼的執行一定是按照某種邏輯執行的,只是有些人能控制自己的程式碼,而有些人則不能。就像我們上學的時候做題一樣,雖然有些題由於我們的大意在沒有看清題意的情況下就直接開始做了,但在正確答案未公佈之前,我們會很堅定自己的答案。儘管最終那道題我們還是得了零分,但當時解題的時候我們肯定是按照某種邏輯來寫算式的,哪怕是按照錯誤的邏輯。這可能也是為什麼有的人明明做錯了事,卻振振有詞的原因吧。

通常情況下,我們所說的異常就是語法錯誤。

2.2 異常繼承結構
Java 異常你必須瞭解的一些知識

考慮到 Java 中只有 Error 和 Exception,並且 Violation 在 Android 中也不常見,因此本篇文章只講解 Error 和 Exception 。

3. 如何在程式中使用異常處理?
3.1 基礎用法
try{
    //有可能出現異常的語句
}catch(異常類 異常物件){
    //異常處理語句
}catch(異常類 異常物件){
    //異常處理語句
}catch(異常類 異常物件){
    //異常處理語句
}
...
finally{
    一定會執行的程式碼
}
複製程式碼

例如:

//原始碼
public class Test20180806 {
	private static String mContent;
	private static String mMSG = "天王蓋地虎";
    public static void main(String []args) {
       try {
			mContent.equals(mMSG);
		} catch (Exception e) {
			e.printStackTrace();
		}
		finally{
			System.out.println(mMSG);
		}
    }
}

//執行結果
java.lang.NullPointerException
	at com.smart.www.Test20180806.main(Test20180806.java:17)
天王蓋地虎
複製程式碼
3.2 注意事項

1) 在 Java 中,所有捕獲範圍小的異常必須放在捕獲範圍大的異常之前,否則程式在編譯時就會出現錯誤提示。

例如:

//原始碼
public class Test20180806 {
	private static String mContent;
	private static String mMSG = "天王蓋地虎";
    public static void main(String []args) {
       try {
			mContent.equals(mMSG);
		} catch (Exception e) {
			e.printStackTrace();
		} catch (NullPointerException e){
			e.printStackTrace();
		}
		finally{
			System.out.println(mMSG);
		}
    }
}

//執行結果
此時開發工具會提醒:  
Unreachable catch block for NullPointerException. It is already handled by the catch block for Exception
複製程式碼

2) 雖然 Throwable 是最大的異常類,但一般在程式中不建議直接使用 Throwable 進行異常捕獲,因為 Throwable 的子類有兩個——Error 和 Exception,Error 本身不需要程式處理,需要程式處理的只有 Exception,所以沒必要使用 Throwable。

//原始碼
//考慮到 Throwable 是 Exception 的父類,因此可以將上面的程式改成下面這個樣子,雖然邏輯上沒有任何問題,但這樣並沒有什麼意義,和上面唯一的區別是:祛除了錯誤提示  
public class Test20180806 {
	private static String mContent;
	private static String mMSG = "天王蓋地虎";
    public static void main(String []args) {
       try {
			mContent.equals(mMSG);
		} catch (Exception e) {
			e.printStackTrace();
		} catch (Throwable e){
			e.printStackTrace();
		}
		finally{
			System.out.println(mMSG);
		}
    }
}
複製程式碼
4. 如何自定義異常?

雖然 Java 中提供了大量的異常類,但有時這些異常可能很難滿足我們的需求,因此我們需要根據自己的情況自定義異常類。自定義異常類的方法很簡單,只需繼承 Exception 即可。

如:

//原始碼
public class TestException20180809 extends Exception {

	public TestException20180809(){}
	
	public TestException20180809(String message){
		super(message);
	}
}

public class Test20180806 {
	private static String mContent;
	private static String mMSG = "天王蓋地虎";
    public static void main(String []args) {
//       try {
//			mContent.equals(mMSG);
//		} catch (Exception e) {
//			e.printStackTrace();
//		} catch (Throwable e){
//			e.printStackTrace();
//		}
//		finally{
//			System.out.println(mMSG);
//		}
       
       try {
    	   throw new TestException20180809();
		} catch (Exception e) {
			e.printStackTrace();
		}
       
       try {
    	   throw new TestException20180809("自定義異常:天王蓋不住地虎!");
		} catch (Exception e) {
			e.printStackTrace();
		}
    }
}

//執行結果
com.smart.www.TestException20180809
	at com.smart.www.Test20180806.main(Test20180806.java:28)
com.smart.www.TestException20180809: 自定義異常:天王蓋不住地虎!
	at com.smart.www.Test20180806.main(Test20180806.java:34)
複製程式碼

在自定義異常 TestException20180809 中,為 TestException20180809 建立了兩個建構函式,通過輸出結果可以看出,二者最大的不同是第二個建構函式中可以新增異常資訊。但這並沒有什麼意義,因為在 99.9% 的情況下,我們只需要知道異常的名稱就足夠了。

5. Throws、Throw 用法
5.1 Throws

在定義一個方法的時候,可以用 Throws 來宣告,被 Throws 宣告過的方法表示此方法不處理異常。

如:

//原始碼
public class Test20180809 {
	public static void main(String []args){
		try {
			divide(10, 0);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	private static void divide(int number1, int number2) throws Exception{
		System.out.println(number1/number2);
	}
}

//執行結果
java.lang.ArithmeticException: / by zero
	at com.smart.www.Test20180809.divide(Test20180809.java:22)
	at com.smart.www.Test20180809.main(Test20180809.java:15)
複製程式碼
5.2 Throw

Throw 表示手動丟擲一個異常,丟擲異常的時候,直接在 Throw 後面新增異常的例項即可。

如:

//原始碼
public class Test201808092037 {

	public static void main(String[] args) {
		try {
			throw new TestException20180809("自定義異常:天王蓋地虎");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
}

//執行結果
com.smart.www.test_exception_20180806.TestException20180809: 自定義異常:天王蓋地虎
	at com.smart.www.test_exception_20180806.Test201808092037.main(Test201808092037.java:16)
複製程式碼
6. 異常執行流程、程式碼塊

異常的執行流程正如錯誤的分類一樣,也有兩種:語法格式和語意格式。

6.1 語法格式
//語法格式
try{
    //有可能出現異常的語句
}catch(異常類 異常物件){
    //異常處理語句
}catch(異常類 異常物件){
    //異常處理語句
}catch(異常類 異常物件){
    //異常處理語句
}
...
finally{
    一定會執行的程式碼
}
複製程式碼
6.2 語意格式

正如 Try…Catch 語法格式一樣,異常的執行流程圖不過是用“語意”的方式將其表達出來。

Java 異常你必須瞭解的一些知識
7. Error 與 Exception、RuntimeException 區別
7.1 Error

Error 未檢查型異常(Unchecked Exception),正常情況下不會出現,因此在定義普通方法或構造方法時不必宣告。

如:

Error:java: Compilation failed: internal java compiler error
複製程式碼

從異常資訊中可以看出,這明顯不是我們的程式可以解決的問題,這是 Java 編譯器和專案中設定的版本不一致導致的異常。

7.2 Exception

Exception 檢查型異常(Checked Exception),在編譯時開發工具會提示用 Try…Catch 語句捕獲,即必須使用使用 Try…Catch 捕獲。

如:

try {
   //如果在程式中不捕獲此異常,開發工具就會提示 Unhandled exception type InterruptedException
   Thread.sleep(1000);
} catch (InterruptedException e) {
   e.printStackTrace();
}
複製程式碼
7.3 RuntimeException

RuntimeException 屬於未檢查型異常(Unchecked Exception),雖是 Exception 的子類,但在程式中不是必須使用使用 Try…Catch 捕獲的,它和 Error 的區別在於,它出現在“正常”情況下。

如:

//原始碼
public class Test20180806 {
	private static String mContent;
	private static String mMSG = "天王蓋地虎";
    public static void main(String []args) {
       try {
			mContent.equals(mMSG);
		} catch (Exception e) {
			e.printStackTrace();
		} catch (Throwable e){
			e.printStackTrace();
		}
		finally{
			System.out.println(mMSG);
		}
    }
}

//執行結果
java.lang.NullPointerException
	at com.smart.www.Test20180806.main(Test20180806.java:17)
天王蓋地虎
複製程式碼
8. 當異常捕獲遇上 Return
8.1 場景分析

異常捕獲和 Return 聯合使用的情況,可以分為如下八種:

序號 具體場景
1 Try 塊中有 Return,Catch 語句中無 Return,Finally 塊中無 Return,方法體有 Return
2 Try 塊中有 Return,Catch 語句中有 Return,Finally 塊中無 Return,方法體無 Return(與 Catch 衝突)
3 Try 塊中有 Return,Catch 語句中無 Return,Finally 塊中有 Return,方法體無 Return(與 Finally 衝突)
4 Try 塊中有 Return,Catch 語句中有 Return,Finally 塊中有 Return,方法體無 Return(與 Finally 衝突)
5 Try 塊中無 Return,Catch 語句中無 Return,Finally 塊中無 Return,方法體有 Return
6 Try 塊中無 Return,Catch 語句中有 Return,Finally 塊中無 Return,方法體有 Return
7 Try 塊中無 Return,Catch 語句中無 Return,Finally 塊中有 Return,方法體無 Return(與 Finally 衝突)
8 Try 塊中無 Return,Catch 語句中有 Return,Finally 塊中有 Return,方法體無 Return(與 Finally 衝突)
8.2 詳細分析
1) Try 塊中有 Return,Catch 語句中無 Return,Finally 塊中無 Return,方法體有 Return
//原始碼  無異常
public class Test201808092310 {
	
	private String mContent;
	
	public static void main(String[] args) {
		System.out.println(returnTest());
	}
	
	private static String returnTest(){
		try {
			System.out.println("Try…");
			return "Try Return";
		} catch (Exception e) {
			System.out.println("Catch…");
		}
		finally {
			System.out.println("Finally…");
		}
		System.out.println("Method…");
		return "Method Feedback";
	}

}
//執行結果
Try…
Finally…
Try Return


//原始碼  有異常
public class Test201808092310 {
	
	private static String mContent;
	
	public static void main(String[] args) {
		System.out.println(returnTest());
	}
	
	private static String returnTest(){
		try {
			System.out.println("Try…");
			System.out.println(mContent.length());
			return "Try Return";
		} catch (Exception e) {
			System.out.println("Catch…");
		}
		finally {
			System.out.println("Finally…");
		}
		System.out.println("Method…");
		return "Method Feedback";
	}

}

//執行結果
Try…
Catch…
Finally…
Method…
Method Feedback
複製程式碼

結論:當 “Try 塊中有 Return,Catch 語句中無 Return,Finally 塊中無 Return,方法體有 Return” 時,無異常出現時,只走 Try 塊、Finally 塊,不走 Method 體,並且由 Try 塊提供返回值;有異常出現時,走 Try 塊、Catch 語句、Finally 塊和 Method 體,並且由 Method 體提供返回值。

2) Try 塊中有 Return,Catch 語句中有 Return,Finally 塊中無 Return,方法體無 Return
//原始碼  無異常  
public class Test201808092310 {
	
	private static String mContent;
	
	public static void main(String[] args) {
		System.out.println(returnTest());
	}
	
	private static String returnTest(){
		try {
			System.out.println("Try…");
			return "Try Return";
		} catch (Exception e) {
			System.out.println("Catch…");
			return "Catch Return";
		}
		finally {
			System.out.println("Finally…");
		}
	}

}

//執行結果  
Try…
Finally…
Try Return  


//原始碼  有異常  
public class Test201808092310 {
	
	private static String mContent;
	
	public static void main(String[] args) {
		System.out.println(returnTest());
	}
	
	private static String returnTest(){
		try {
			System.out.println("Try…");
			System.out.println(mContent.length());
			return "Try Return";
		} catch (Exception e) {
			System.out.println("Catch…");
			return "Catch Return";
		}
		finally {
			System.out.println("Finally…");
		}
		//此處的程式碼開發工具會提示:Unreachable code,故作刪除處理
	}

}

//執行結果  
Try…
Catch…
Finally…
Catch Return
複製程式碼

結論:當 “Try 塊中有 Return,Catch 語句中有 Return,Finally 塊中無 Return,方法體無 Return” 時,無異常出現時,只走 Try 塊、Finally 塊,不走 Catch 語句,並且由 Try 塊提供返回值;有異常出現時,走 Try 塊、Catch 語句、Finally 塊,並且由 Catch 語句提供返回值。

3) Try 塊中有 Return,Catch 語句中無 Return,Finally 塊中有 Return,方法體無 Return
//原始碼  無異常
public class Test201808092310 {
	
	private static String mContent;
	
	public static void main(String[] args) {
		System.out.println(returnTest());
	}
	
	private static String returnTest(){
		try {
			System.out.println("Try…");
			return "Try Return";
		} catch (Exception e) {
			System.out.println("Catch…");
		}
		finally {
			System.out.println("Finally…");
			return "Finally Return";
		}
	}

}

//執行結果
Try…
Finally…
Finally Return  


//原始碼  有異常
public class Test201808092310 {
	
	private static String mContent;
	
	public static void main(String[] args) {
		System.out.println(returnTest());
	}
	
	private static String returnTest(){
		try {
			System.out.println("Try…");
			System.out.println(mContent.length());
			return "Try Return";
		} catch (Exception e) {
			System.out.println("Catch…");
		}
		finally {
			System.out.println("Finally…");
			return "Finally Return";
		}
	}

}

//執行結果
Try…
Catch…
Finally…
Finally Return
複製程式碼

結論:當 “Try 塊中有 Return,Catch 語句中無 Return,Finally 塊中有 Return,方法體無 Return” 時,無異常出現時,只走 Try 塊、Finally 塊,不走 Catch 語句,並且由 Finally 塊提供返回值;有異常出現時,走 Try 塊、Catch 語句、Finally 塊,並且由 Finally 塊提供返回值。

4) Try 塊中有 Return,Catch 語句中有 Return,Finally 塊中有 Return,方法體無 Return
//原始碼  無異常  
public class Test201808092310 {
	
	private static String mContent;
	
	public static void main(String[] args) {
		System.out.println(returnTest());
	}
	
	private static String returnTest(){
		try {
			System.out.println("Try…");
			
			return "Try Return";
		} catch (Exception e) {
			System.out.println("Catch…");
			return "Catch Return";
		}
		finally {
			System.out.println("Finally…");
			return "Finally Return";
		}
	}

}

//執行結果
Try…
Finally…
Finally Return  


//原始碼  有異常  
public class Test201808092310 {
	
	private static String mContent;
	
	public static void main(String[] args) {
		System.out.println(returnTest());
	}
	
	private static String returnTest(){
		try {
			System.out.println("Try…");
			System.out.println(mContent.length());
			return "Try Return";
		} catch (Exception e) {
			System.out.println("Catch…");
			return "Catch Return";
		}
		finally {
			System.out.println("Finally…");
			return "Finally Return";
		}
	}

}

//執行結果
Try…
Catch…
Finally…
Finally Return
複製程式碼

結論:當 “Try 塊中有 Return,Catch 語句中有 Return,Finally 塊中有 Return,方法體無 Return” 時,無異常出現時,只走 Try 塊、Finally 塊,不走 Catch 語句,並且由 Finally 塊提供返回值;有異常出現時,走 Try 塊、Catch 語句、Finally 塊,並且由 Finally 塊提供返回值。

5) Try 塊中無 Return,Catch 語句中無 Return,Finally 塊中無 Return,方法體有 Return
//原始碼  無異常  
public class Test201808092310 {
	
	private static String mContent;
	
	public static void main(String[] args) {
		System.out.println(returnTest());
	}
	
	private static String returnTest(){
		try {
			System.out.println("Try…");
		} catch (Exception e) {
			System.out.println("Catch…");
		}
		finally {
			System.out.println("Finally…");
		}
		System.out.println("Method…");
		return "Method Feedback";
	}

}

//執行結果
Try…
Finally…
Method…
Method Feedback


//原始碼  有異常  
public class Test201808092310 {
	
	private static String mContent;
	
	public static void main(String[] args) {
		System.out.println(returnTest());
	}
	
	private static String returnTest(){
		try {
			System.out.println("Try…");
			System.out.println(mContent.length());
		} catch (Exception e) {
			System.out.println("Catch…");
		}
		finally {
			System.out.println("Finally…");
		}
		System.out.println("Method…");
		return "Method Feedback";
	}

}

//執行結果
Try…
Catch…
Finally…
Method…
Method Feedback
複製程式碼

結論:當 “Try 塊中無 Return,Catch 語句中無 Return,Finally 塊中無 Return,方法體有 Return” 時,無異常出現時,只走 Try 塊、Finally 塊和 Method 體,不走 Catch 語句,並且由 Method 體提供返回值;有異常出現時,走 Try 塊、Catch 語句、Finally 塊和 Method 體,並且由 Method 體提供返回值。

6) Try 塊中無 Return,Catch 語句中有 Return,Finally 塊中無 Return,方法體有 Return
//原始碼  無異常  
public class Test201808092310 {
	
	private static String mContent;
	
	public static void main(String[] args) {
		System.out.println(returnTest());
	}
	
	private static String returnTest(){
		try {
			System.out.println("Try…");
		} catch (Exception e) {
			System.out.println("Catch…");
			return "Catch Return";
		}
		finally {
			System.out.println("Finally…");
		}
		System.out.println("Method…");
		return "Method Feedback";
	}

}
//執行結果
Try…
Finally…
Method…
Method Feedback


//原始碼  有異常
public class Test201808092310 {
	
	private static String mContent;
	
	public static void main(String[] args) {
		System.out.println(returnTest());
	}
	
	private static String returnTest(){
		try {
			System.out.println("Try…");
			System.out.println(mContent.length());
		} catch (Exception e) {
			System.out.println("Catch…");
			return "Catch Return";
		}
		finally {
			System.out.println("Finally…");
		}
		System.out.println("Method…");
		return "Method Feedback";
	}

}
//執行結果
Try…
Catch…
Finally…
Catch Return
複製程式碼

結論:當 “Try 塊中無 Return,Catch 語句中有 Return,Finally 塊中無 Return,方法體有 Return” 時,無異常出現時,只走 Try 塊、Finally 塊和 Method 體,不走 Catch 語句,並且由 Method 體提供返回值;有異常出現時,走 Try 塊、Catch 語句、Finally 塊,並且由 Catch 語句提供返回值。

7) Try 塊中無 Return,Catch 語句中無 Return,Finally 塊中有 Return,方法體無 Return
//原始碼  無異常
public class Test201808092310 {
	
	private static String mContent;
	
	public static void main(String[] args) {
		System.out.println(returnTest());
	}
	
	private static String returnTest(){
		try {
			System.out.println("Try…");
		} catch (Exception e) {
			System.out.println("Catch…");
		}
		finally {
			System.out.println("Finally…");
			return "Finally Return";
		}
	}

}
//執行結果
Try…
Finally…
Finally Return


//原始碼  有異常
public class Test201808092310 {
	
	private static String mContent;
	
	public static void main(String[] args) {
		System.out.println(returnTest());
	}
	
	private static String returnTest(){
		try {
			System.out.println("Try…");
			System.out.println(mContent.length());
		} catch (Exception e) {
			System.out.println("Catch…");
		}
		finally {
			System.out.println("Finally…");
			return "Finally Return";
		}
	}

}
//執行結果
Try…
Catch…
Finally…
Finally Return
複製程式碼

結論:當 “Try 塊中無 Return,Catch 語句中無 Return,Finally 塊中有 Return,方法體無 Return” 時,無異常出現時,只走 Try 塊、Finally 塊,不走 Catch 語句、 Method 體,並且由 Finally 塊提供返回值;有異常出現時,走 Try 塊、Catch 語句、Finally 塊,並且由 Finally 塊提供返回值。

8) Try 塊中無 Return,Catch 語句中有 Return,Finally 塊中有 Return,方法體無 Return
//原始碼  無異常
public class Test201808092310 {
	
	private static String mContent;
	
	public static void main(String[] args) {
		System.out.println(returnTest());
	}
	
	private static String returnTest(){
		try {
			System.out.println("Try…");
		} catch (Exception e) {
			System.out.println("Catch…");
			return "Catch Return";
		}
		finally {
			System.out.println("Finally…");
			return "Finally Return";
		}
	}

}
//執行結果
Try…
Finally…
Finally Return


//原始碼  有異常
public class Test201808092310 {
	
	private static String mContent;
	
	public static void main(String[] args) {
		System.out.println(returnTest());
	}
	
	private static String returnTest(){
		try {
			System.out.println("Try…");
			System.out.println(mContent.length());
		} catch (Exception e) {
			System.out.println("Catch…");
			return "Catch Return";
		}
		finally {
			System.out.println("Finally…");
			return "Finally Return";
		}
	}

}

//執行結果
Try…
Catch…
Finally…
Finally Return
複製程式碼

結論:當 “Try 塊中無 Return,Catch 語句中有 Return,Finally 塊中有 Return,方法體無 Return” 時,無異常出現時,只走 Try 塊、Finally 塊,不走 Catch 語句、 Method 體,並且由 Finally 塊提供返回值;有異常出現時,走 Try 塊、Catch 語句、Finally 塊,並且由 Finally 塊提供返回值。

綜合以上八條,可以得出一個結論:在一個方法中,無論 Try 塊中有沒有異常、Return,只要 Finally 塊中有 Return,那麼函式的返回值都由 Finally 塊提供。


參考文件

1)《Java 開發實戰經典》
2)《Thinking in Java》
3)《Android Developer Document》

相關文章