Java基礎 — 異常

huizhe發表於2019-02-10

異常物件都是派生於Throwable 類的一個例項。

異常層次結構簡化示意圖:

異常層次結構簡化示意圖

所有的異常都是由Throwable 繼承而來,但在下一層立即分解為兩個分支:Error 和 Exception

Error類層次結構描述了Java執行時系統的內部錯誤資源耗盡錯誤

Exception 層次

Exception 層次分解為兩個分支: RuntimeException其他異常

RuntimeException 異常:

  1. 錯誤的型別轉換 ClassCastException
  2. 陣列訪問越界 ArrayIndexOutOfBoundsException
  3. 訪問null指標 NullPointerException

不是派生於 RuntimeException 異常包括:

  1. 試圖在檔案尾部後面讀取資料
  2. 試圖開啟一個不存在的檔案
  3. 試圖根據給定的字串查詢Class物件,而這個字串表示的類並不存在

Java語言規範將派生於 Error 異常或 RuntimeException 類的所有異常稱為非受查(unchecked) 異常

所有其他的異常稱為受查(checked)異常

什麼時候該丟擲異常 throws

  1. 呼叫一個丟擲受查異常的方法
  2. 執行時發現錯誤,利用 throw 語句丟擲一個受查異常
  3. 程式出現錯誤,如 ArrayIndexOutOfBoundsException 這樣的非受查異常
  4. Java 虛擬機器和執行時庫出現的內部錯誤

子類方法中宣告的受查異常並不能比超類方法中宣告的異常更通用,即子類方法中可丟擲更特定的異常,或者根本不丟擲任何異常。特別宣告:如果超類方法沒有丟擲任何受查異常,子類也不能丟擲任何受查異常。

自定義異常:

  1. 定義一個派生於Exception的類,或者派生於Exception子類的類。
  2. 習慣上,定義的類應該包含兩個構造器。一個預設的構造器,另一個是帶有詳細描述資訊構造器。
class FileFormatException extends IOException
{
	public FileFormatException(){}
	public FileFormatException(String message)
	{
		super(message);
	}
}
複製程式碼

API java.lang.Throwable

  • Throwable() 構造一個新的Throwable 物件,這個物件沒有詳細的描述資訊
  • Throwable(String message) 構造一個新的Throwable,這個物件帶有特定的詳細描述資訊。習慣上,所有的派生的異常類都支援一個預設的構造器和一個帶有詳細資訊的構造器。
  • String getMessage() 獲得Throwable 物件的詳細描述資訊

異常處理小技巧

一般異常處理最好的選擇,就是將異常傳遞給呼叫者,讓呼叫者自己去操心。

在catch 字句中可以丟擲一個異常,這樣做的目的是改變異常的型別。我們可以採用一種比較推薦的處理異常的方法,並且將原始異常設定為新異常的”原因”:

try
{
	access the database
}
catch(SQLException e)
{
	Throwable se = new ServletException("database error");
	se.initCause(e);
	throw se;
}
複製程式碼

當捕獲到異常時,就可以使用下面這條語句重新得到原始異常:

Throwable e = se.getCause();
複製程式碼

使用這種包裝技術,可讓使用者丟擲子系統中的高階異常,而不會丟失原始異常的細節

如果在一個方法中發生了一個受查異常,而不允許丟擲它,那包裝技術就十分有用。我們可捕獲這個受查異常,並將它包裝成一個執行時異常。

finally 語句

不管是否有異常被捕獲,finally 字句中的程式碼都被執行。

當finally字句包含return 語句時,將會出現一種意想不到的結果。

假設利用return 語句從try語句塊中退出。在方法返回前,finally字句的內容將被執行。如果finally字句中也有一個return語句,這個返回值將會覆蓋原始的返回值。例:

 public static int f(int n)
{
    try{
        return n*n;
    }finally {
        if (2 == n)
            return 0;
    }
}
複製程式碼

如果呼叫f(2) ,try語句返回結果為4,然而在方法返回前,要執行finally字句。finally字句使得方法返回0。這個返回值覆蓋了原先的返回值4。所以呼叫 f(2) 返回的值為 0

JAVA SE7 關閉資源的處理

待資源的try 語句(try-with-resources) 的最簡形式

try(Resource res = ...)
{
	work with res
}
複製程式碼

try 塊退出時,會自動呼叫res.close()

指定多個資源:

try(Scanner in = new Scanner(new FileInputStream("/usr/shar/dict/words"),"UTF-8");
        PrintWriter out = new PrintWriter("out.txt")){
            while (in.hasNext())
            out.println(in.next().toUpperCase());
}
複製程式碼

不管這個塊如何退出,in 和 out 都會關閉。

常規方式手動程式設計,就需要兩個巢狀的try/finally 語句。

堆疊軌跡(stack trace)

堆疊軌跡是一個方法呼叫過程的列表,它包含了程式執行過程中方法呼叫的特定位置。

訪問堆疊軌跡的文字描述資訊

Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for (StackTraceElement frame : frames){
	analyze frame
}
複製程式碼

StackTraceElement 類含有能夠獲得檔名和當前執行的程式碼行號的方法。同時,還含有能夠獲得類名和方法名的方法。

靜態的 Thread.getAllStackTrace 方法,它可以產生所有執行緒的堆疊軌跡。例

Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
for (Thread t : map.keySet()){
    StackTraceElement[] frames = map.get(t);
    analyze frames
}
複製程式碼

列印一個遞迴階乘的函式的堆疊情況

public class StackTraceTest {

    /**
     * 計算n的階乘
     * @param n
     * @return
     */
    public static int factorial(int n)
    {
        System.out.println("factorial(" + n + "):");
        Throwable t = new Throwable();
        StackTraceElement[] frames = t.getStackTrace();
        for (StackTraceElement f: frames)
            System.out.println(f);
        int r;
        if (n<=1)
            r =1;
        else
            r = n * factorial(n-1);
        System.out.println("return " + r);
        return r;
    }
    public static void main(String[] args)
    {
        Scanner in = new Scanner(System.in);
        System.out.print("Enter n : ");
        int n = in.nextInt();
        factorial(n);
    }
}
複製程式碼

計算factorial(3),列印一下內容

factorial(3):
javabook.StackTraceTest.factorial(StackTraceTest.java:15)
javabook.StackTraceTest.main(StackTraceTest.java:32)
factorial(2):
javabook.StackTraceTest.factorial(StackTraceTest.java:15)
javabook.StackTraceTest.factorial(StackTraceTest.java:23)
javabook.StackTraceTest.main(StackTraceTest.java:32)
factorial(1):
javabook.StackTraceTest.factorial(StackTraceTest.java:15)
javabook.StackTraceTest.factorial(StackTraceTest.java:23)
javabook.StackTraceTest.factorial(StackTraceTest.java:23)
javabook.StackTraceTest.main(StackTraceTest.java:32)
return 1
return 2
return 6
複製程式碼

使用異常小技巧

異常處理不能代替簡單的測試

與執行簡單的測試相比,捕獲異常所花費的時間大大超過前者。因此使用異常的基本規則是,旨在異常情況下使用異常機制。

不要過分地細化異常

將整個任務包裝在一個try塊中,這樣,當任何一個操作出現問題時,整個任務都可以取消。

利用異常層次結構

  • 不要只丟擲 RuntimeException 異常。應該尋找更加適當的子類或建立自己的異常類。
  • 不要只捕獲Throwable 異常,否則,會使程式程式碼更難讀、更難維護
  • 考慮受查異常和非受查異常的區別。
  • 將一種異常轉換成另一種更加適合的異常時不要猶豫。

不要壓制異常

在java中,往往強化地傾向關閉異常。

在檢測錯誤時,“苛刻”要比放任更好

例如,當棧空時,Stack.pop 是要返回一個null,還是丟擲一個異常?我們認為:在出錯的地方丟擲一個 EmptyStackException異常要比在後面丟擲一個 NullPointerException 異常更好。

不要羞於傳遞異常

讓高層次的方法通知使用者發生了錯誤,或者放棄不成功的命令更加適宜。

5 和 6 可以歸納為“早丟擲,晚捕獲”。

參考

JAVA核心技術(卷1)原書第10版

相關文章