堆疊追蹤


在重重的方法呼叫下,例外發生的點可能是在某個方法之中,若想得知例外發生的根源,以及重重方法呼叫下傳播的呼叫堆疊,則可以利用例外物件所自動收集的堆疊追蹤(Stack Trace)來取得相關的資訊。

最簡單的方法,就是直接呼叫例外物件的printStackTrace()來顯示堆疊追蹤。例如:
public class Main {   
    public static String a() {
        String text = null;
        return text.toUpperCase();
    }
    public static void b() {
        a();
    }
    public static void c() {
        b();
    }
    public static void main(String[] args) {
        try {
            c();
        }
        catch(NullPointerException ex) {
            ex.printStackTrace();
        }
    }
}

這個程式中,c()方法呼叫b()方法,b()方法呼叫a()方法,而a()方法中會引發NullPointerException,假設你並不知道這個呼叫的順序(也許你是在使用一個程式庫),當例外發生而被捕捉後,你可以呼叫printStackTrace()在主控台(Console)顯示堆疊追蹤:
java.lang.NullPointerException
        at Main.a(Main.java:4)
        at Main.b(Main.java:8)
        at Main.c(Main.java:12)
        at Main.main(Main.java:17)

堆疊追蹤訊息中顯示了例外的類型,最頂層是例外的根源,以下是呼叫方法的順序,所顯示的程式碼行數,是對應於當初你的程式原始碼。printStackTrace()還有接受PrintStream、PrintWriter的版本,可以讓你將堆疊追蹤訊息以指定的IO輸出。

如果想要取得個別的堆疊追蹤元素進行處理,則可以使用getStackTrace(),這會傳回StackTraceElement陣列,陣列中索引0為例外根源的相關資訊,之後為各方法呼叫中的資訊,可以使用StackTraceElement的getClassName()、getFileName()、getLineNumber()、getMethodName()等方法取得對應的資訊。

 要善用堆疊追蹤,前題是你不能在程式碼中有私吞例外的行為,例如在捕捉例外後什麼都不作:
try {
    ...
} catch(SomeException ex) {
    // 什麼也沒有
}

這樣的程式碼之所以會對應用程式造成嚴重的傷害,就在於例外訊息完全中止在這邊,之後呼叫此片段程式碼的客戶端,完全不知道發生了什麼事,在除錯時會異常困難,甚至找不出錯誤的根源,另一種就是對例外作了不適當的處理,或顯示了不正確的資訊,例如有些時候,由於某個例外階層下引發的例外類型很多,例如:
try {
    ...
} catch(FileNotFoundException ex) {
    // ...
} catch(EOFException ex) {
    // ...
}

有些程式設計人員為了省麻煩,就會寫成這樣:
try {
    ...
} catch(IOException ex) {
    ...
}

而後或許在找不到檔案時,覺得需要顯示些訊息:
try {
    ...
} catch(IOException ex) {
    System.out.println("找不到檔案");
}

類似這樣的程式碼,在專案中履見不鮮,假以時日,或者是別人在除錯時,發現錯誤訊息是找不到檔案,因而誤導了除錯的方向,事實上可能其它原因(IOException的其它子類型例外)。

在使用throw重拋例外時,例外的追蹤堆疊起點,仍是例外的發生根源,而不是重拋例外的地方。例如:
public class Main {   
    public static String a() {
        String text = null;
        return text.toUpperCase();
    }   
    public static void b() {
        a();
    }   
    public static void c() {
        try {
            b();
        } catch(NullPointerException ex) {
            ex.printStackTrace();
            System.out.println("重拋例外");
            throw ex;
        }
       
    }   
    public static void main(String[] args) {
        try {
            c();
        }
        catch(NullPointerException ex) {
            ex.printStackTrace();
        }
    }
}

執行這個程式,會發生以下的例外堆疊訊息:
java.lang.NullPointerException
        at Main.a(Main.java:4)
        at Main.b(Main.java:8)
        at Main.c(Main.java:13)
        at Main.main(Main.java:24)
重拋例外
java.lang.NullPointerException
        at Main.a(Main.java:4)
        at Main.b(Main.java:8)
        at Main.c(Main.java:13)
        at Main.main(Main.java:24)

如果你想要讓例外堆疊起點為重拋例外的地方,則可以使用fillInStackTrace(),這個方法會重新裝填例外堆疊,將起點設為重拋例外的地方,並傳回Throwable物件。例如:
public class Main {   
    public static String a() {
        String text = null;
        return text.toUpperCase();
    }
    public static void b() {
        a();
    }
    public static void c() {
        try {
            b();
        } catch(NullPointerException ex) {
            ex.printStackTrace();
            System.out.println("重拋例外");
            throw (NullPointerException) ex.fillInStackTrace();
        }
       
    }
    public static void main(String[] args) {
        try {
            c();
        }
        catch(NullPointerException ex) {
            ex.printStackTrace();
        }
    }
}

執行這個程式,會發生以下的訊息:
java.lang.NullPointerException
        at Main.a(Main.java:4)
        at Main.b(Main.java:8)
        at Main.c(Main.java:13)
        at Main.main(Main.java:24)
重拋例外
java.lang.NullPointerException
        at Main.c(Main.java:17)
        at Main.main(Main.java:24)