認識堆疊追蹤


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

查看堆疊追蹤最簡單的方法,就是直接呼叫例外物件的printStackTrace()。例如:

package cc.openhome;

public class Main {   
    public static void main(String[] args) {
        try {
            c();
        } catch(NullPointerException ex) {
            ex.printStackTrace();
        }
    }

    static void c() {
        b();
    }

    static void b() {
        a();
    }

    static String a() {
        String text = null;
        return text.toUpperCase();
    }
}

這個範例程式中,c()方法呼叫b()方法,b()方法呼叫a()方法,而a()方法中會因text參考至null,而後試圖呼叫toUpperCase()而引發NullPointerException,假設事先並不知道這個呼叫的順序(也許你是在使用一個程式庫),當例外發生而被捕捉後,可以呼叫printStackTrace()在主控台顯示堆疊追蹤:

java.lang.NullPointerException
        at cc.openhome.Main.a(Main.java:22)
        at cc.openhome.Main.b(Main.java:17)
        at cc.openhome.Main.c(Main.java:13)
        at cc.openhome.Main.main(Main.java:6)

堆疊追蹤訊息中顯示了例外類型,最頂層是例外的根源,以下是呼叫方法的順序,程式碼行數是對應於當初的程式原始碼,如果使用IDE,按下行數就會直接開啟原始碼並跳至對應行數(如果原始碼存在的話)。printStackTrace()還有接受PrintStreamPrintWriter的版本,可以將堆疊追蹤訊息以指定方式至輸出目的地(例如檔案)。

編譯位元碼檔案時,預設會記錄原始碼行數資訊等除錯資訊,在使用javac編譯時指定-g:none引數就不會記錄除錯資訊,編譯出來的位元碼檔案容量會比較小。

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

要善用堆疊追蹤,前題是程式碼中不可有私吞例外的行為,例如在捕捉例外後什麼都不作:

try {
    ...
} catch(SomeException ex) {
    // 什麼也沒有,絕對不要這麼作!
}

這樣的程式碼會對應用程式維護造成嚴重傷害,因為例外訊息會完全中止,之後呼叫此片段程式碼的客戶端,完全不知道發生了什麼事,造成除錯異常困難,甚至找不出錯誤根源。

另一種對應用程式維護會有傷害的方式,就是對例外作了不適當的處理,或顯示了不正確的資訊。例如,有時由於某個例外階層下引發的例外類型很多:

try {
    ...
} catch(FileNotFoundException ex) {
    作一些處理
} catch(EOFException ex) {
    作一些處理
}

有些程式設計人員為了省麻煩,或因為經常處理找不到檔案的錯誤,因而寫成這樣:

try {
    ...
} catch(IOException ex) {
    System.out.println("找不到檔案");
}
   
這類的程式碼在專案中還蠻常見的,假以時日或者是別人使用程式時,真的發生了EOFException(或其它原因導致了IOException或其子類型例外),但錯誤訊息卻會一直顯示找不到檔案,因而誤導了除錯的方向。

在使用throw重拋例外時,例外的追蹤堆疊起點,仍是例外的發生根源,而不是重拋例外的地方。例如:

package cc.openhome;

public class Main2 { 
    public static void main(String[] args) {
        try {
            c();
        } catch(NullPointerException ex) {
            ex.printStackTrace();
        }
    }

    static void c() {
        try {
            b();
        } catch(NullPointerException ex) {
            ex.printStackTrace();
            throw ex;
        }
       
    }  

    static void b() {
        a();
    }   

    static String a() {
        String text = null;
        return text.toUpperCase();
    }   
}

執行這個程式,會發生以下的例外堆疊訊息,可看到兩次都是顯示相同的堆疊訊息:

java.lang.NullPointerException
        at cc.openhome.Main2.a(Main2.java:28)
        at cc.openhome.Main2.b(Main2.java:23)
        at cc.openhome.Main2.c(Main2.java:14)
        at cc.openhome.Main2.main(Main2.java:6)
java.lang.NullPointerException
        at cc.openhome.Main2.a(Main2.java:28)
        at cc.openhome.Main2.b(Main2.java:23)
        at cc.openhome.Main2.c(Main2.java:14)
        at cc.openhome.Main2.main(Main2.java:6)

如果想要讓例外堆疊起點為重拋例外的地方,可以使用fillInStackTrace(),這個方法會重新裝填例外堆疊,將起點設為重拋例外的地方,並傳回Throwable物件。例如:

package cc.openhome;

public class Main3 {
    public static void main(String[] args) {
        try {
            c();
        } catch(NullPointerException ex) {
            ex.printStackTrace();
        }
    }  

    static void c() {
        try {
            b();
        } catch(NullPointerException ex) {
            ex.printStackTrace();
            Throwable t = ex.fillInStackTrace();
            throw (NullPointerException) t;
        }
    }

    static void b() {
        a();
    }

    static String a() {
        String text = null;
        return text.toUpperCase();
    }  
}

執行這個程式,會發生以下的訊息,可看到第二次顯示堆疊追蹤的起點,就是重拋例外的起點:

java.lang.NullPointerException
        at cc.openhome.Main3.a(Main3.java:28)
        at cc.openhome.Main3.b(Main3.java:23)
        at cc.openhome.Main3.c(Main3.java:14)
        at cc.openhome.Main3.main(Main3.java:6)
java.lang.NullPointerException
        at cc.openhome.Main3.c(Main3.java:17)
        at cc.openhome.Main3.main(Main3.java:6)