Java原始碼分析——Throwable、Exception、類解析
在Java中,錯誤分為兩種,一種是jvm能處理的錯誤,叫做異常,Java中表示Exception類;而另外一種則是jvm不能處理的錯誤,叫做錯誤,Java中表示為Error類。它們三者的關係以及常見的子類的實現如下圖:
Throwable類是Java中一切Exception類與Error類的父類,它直接以Native方法與jvm進行互動,從jvm中獲取java程式執行時的異常和錯誤資訊,並在console中打印出來,其中它的主要成員與構造方法如下:
public class Throwable implements Serializable {
//用來儲存一些棧的回溯點
private transient Object backtrace;
//對throwble的描述
private String detailMessage;
//引發該throwable的Throwable類,預設為自身
private Throwable cause = this;
//用棧來儲存異常的發生順序
private StackTraceElement[] stackTrace = UNASSIGNED_STACK;
public Throwable() {
fillInStackTrace();
}
public Throwable (String message) {
fillInStackTrace();
detailMessage = message;
}
public Throwable(String message, Throwable cause) {
fillInStackTrace();
detailMessage = message;
this.cause = cause;
}
public Throwable(Throwable cause) {
fillInStackTrace ();
detailMessage = (cause==null ? null : cause.toString());
this.cause = cause;
}
protected Throwable(String message, Throwable cause,
boolean enableSuppression,
boolean writableStackTrace) {
if (writableStackTrace) {
fillInStackTrace();
} else {
stackTrace = null;
}
detailMessage = message;
this.cause = cause;
if (!enableSuppression)
suppressedExceptions = null;
}
public String getMessage() {
return detailMessage;
}
//獲取其異常的資訊
public String getLocalizedMessage() {
return getMessage();
}
//返回引發異常的原因異常類
public synchronized Throwable getCause() {
return (cause==this ? null : cause);
}
}
其中的cause成員變數是用來存貯引發某個異常或錯誤的異常或錯誤類的,也就是說它儲存著引發異常或錯誤的原因異常類,如果沒有賦值的話,預設是其自身,預設沒有造成當前異常的異常類,也就是getCause方法等於預設的時候,返回的是null的原因,而且getCause是一個同步方法,保證了執行緒的安全,因為異常或錯誤資訊的返回是有一個延時性的,防止了返回資訊的時候遭到了其他子執行緒的修改。這也形成了一個因果關係,並且可以形成一條因果異常鏈,即我的throable是你引起的,而你的thowable是他引起的,這就構成了一條因果鏈了。而fillInStackTrace方法則是呼叫了Native方法,從jvm中將程式執行軌跡進棧,並返回一個異常,這個棧是在jvm中的,而在java程式中是用StackTraceElement棧來儲存:
public synchronized Throwable fillInStackTrace() {
if (stackTrace != null ||
backtrace != null /* Out of protocol state */ ) {
fillInStackTrace(0);
stackTrace = UNASSIGNED_STACK;
}
return this;
}
//將程式執行軌跡進棧,並返回一個異常
private native Throwable fillInStackTrace(int dummy);
從jvm中取出程式執行的軌跡的程式碼如下:
//獲取棧中某個元素
native StackTraceElement getStackTraceElement(int index);
//獲取棧的長度
native int getStackTraceDepth();
public StackTraceElement[] getStackTrace() {
return getOurStackTrace().clone();
}
private synchronized StackTraceElement[] getOurStackTrace() {
// Initialize stack trace field with information from
// backtrace if this is the first call to this method
if (stackTrace == UNASSIGNED_STACK ||
(stackTrace == null && backtrace != null) /* Out of protocol state */) {
int depth = getStackTraceDepth();
stackTrace = new StackTraceElement[depth];
for (int i=0; i < depth; i++)
stackTrace[i] = getStackTraceElement(i);
} else if (stackTrace == null) {
return UNASSIGNED_STACK;
}
return stackTrace;
}
StackTraceElement棧儲存的主要元素有:
public final class StackTraceElement implements java.io.Serializable {
private String declaringClass;
private String methodName;
private String fileName;
private int lineNumber;
}
從原始碼中可以得出,StackTraceElement儲存的是每個類描述、呼叫這個類的方法、該類的檔名、以及程式碼行數。也就是說StackTraceElement儲存的是從fillInStackTrace方法得到的程式執行軌跡中的類與方法的執行順序情況。而在列印異常或錯誤資訊的時候,是按照棧的先進後出的原則列印的,而且列印方法是遞迴列印的:
private static final String NULL_CAUSE_MESSAGE = "Cannot suppress a null exception.";
private static final String SELF_SUPPRESSION_MESSAGE = "Self-suppression not permitted";
private static final String CAUSE_CAPTION = "Caused by: ";
private static final String SUPPRESSED_CAPTION = "Suppressed: ";
private void printStackTrace(PrintStreamOrWriter s) {
······
//同步
synchronized (s.lock()) {
//列印有該異常類的造成影響的異常類,也就是上層的異常類
for (Throwable se : getSuppressed())
se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION, "\t", dejaVu);
//列印造成該異常類的異常類,如果有的話
Throwable ourCause = getCause();
if (ourCause != null)
ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, "", dejaVu);
}
}
private void printEnclosedStackTrace(PrintStreamOrWriter s,
StackTraceElement[] enclosingTrace,
String caption,
String prefix,
Set<Throwable> dejaVu) {
//確保有鎖
assert Thread.holdsLock(s.lock());
//從棧頂開始遍歷
int m = trace.length - 1;
int n = enclosingTrace.length - 1;
while (m >= 0 && n >=0 && trace[m].equals(enclosingTrace[n])) {
m--; n--;
}
······
for (Throwable se : getSuppressed())
se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION,
prefix +"\t", dejaVu);
Throwable ourCause = getCause();
if (ourCause != null)
ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, prefix, dejaVu);
}
}
其中的getSuppressed方法是返回一個該異常類的造成影響的異常類的陣列,也就是該異常類的上層異常類,而getCause是返回造成當前異常類的異常類,是比該異常類還下層的異常類,也就是上面說的,形成一個異常鏈。而棧的先進後出是異常的丟擲往往能正確的指定到產生異常的地方的原因,因為這時候程式執行的程式碼(最新的異常或錯誤)是最後進棧的,而列印的時候是最先列印的,列印方法也保證了執行緒的安全。而Throwable原始碼中的initCause方法是來設定引起當前異常類的異常類的,一個Throwable類只能設定一次cause:
public synchronized Throwable initCause(Throwable cause) {
if (this.cause != this)//已設定過cause,則拋異常,意思為只能設定一次
cause屬性
throw new IllegalStateException("Can't overwrite cause");
if (cause == this)//若引數cause等於本身,拋異常
throw new IllegalArgumentException("Self-causation not permitted");
this.cause = cause;
return this;
}
其中異常分為兩種,一種是執行時異常,即Runtime異常,一種是檢查時異常,即Checked異常。在上圖中除了RuntimeException及其子類是Runtime異常,其餘的都是Checked異常。 兩者的區別是:Checked Exception用來指示一種呼叫方能夠直接處理的異常情況,比如類未找到異常等。 而Runtime Exception則用來指示一種呼叫方本身無法處理或恢復的程式錯誤,比如除零異常等。Exception類與Error類只是簡單的繼承了Throwable類,以Exception類為例:
public class Exception extends Throwable {
static final long serialVersionUID = -3387516993124229948L;
public Exception() {
super();
}
public Exception(String message) {
super(message);
}
public Exception(String message, Throwable cause) {
super(message, cause);
}
public Exception(Throwable cause) {
super(cause);
}
protected Exception(String message, Throwable cause,
boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
那麼如何自定義一個Exception類呢?其實很簡單,只要繼承Exception類,在裡面加你自定義類的描述資訊就夠了,如下:
public class TestException extends Exception{
private String message;
public TestException(String message){
this.message=message;
}
public String toString(){
return message;
}
}
public class Test {
public static void main(String args[]) throws TestException {
int a=9;
if(a<10){
throw new TestException("111");
}
}
}
//輸出:Exception in thread "main" 111
//at test.Test.main(Test.java:10)
還有一點注意的是,catch與finally語句塊並不會因為return的操作而受到影響:
public static void main(String args[]) throws TestException {
try{
int b=10/0;
return;
}catch (Exception e){
System.out.println("中間");
return;
}finally {
System.out.println("終結");
}
}
//輸出為:中間 終結