在多重方法呼叫下,例外發生點可能是在某個方法之中,若想得知例外發生的根源,以及多重方法呼叫下例外的堆疊傳播,可以利用例外物件自動收集的堆疊追蹤(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)
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()
還有接受PrintStream
、PrintWriter
的版本,可以將堆疊追蹤訊息以指定方式至輸出目的地(例如檔案)。編譯位元碼檔案時,預設會記錄原始碼行數資訊等除錯資訊,在使用
javac
編譯時指定-g:none
引數就不會記錄除錯資訊,編譯出來的位元碼檔案容量會比較小。如果想要取得個別的堆疊追蹤元素進行處理,則可以使用
getStackTrace()
,這會傳回StackTraceElement
陣列,陣列中索引0為例外根源的相關資訊,之後為各方法呼叫中的資訊,可以使用StackTraceElement
的getClassName()
、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)
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)
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)