Java源码 - Exceotion异常类的基类Throwable分析

常用的异常有:Error、Exception,这两个类的基类都是Throwable。

其中Error是用来表示编译时和系统的错误,这一类问题,基本不需要我们关心。

Exception就是我们常见的异常。由源码可知,Exception类自身并没有什么重要的东西,它只是Throwable类的一个子类。

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);
    }
}

因此我们学习异常的源码重心就放在了Throwable类上。

一个异常类,主要的成员属性有:异常名,以及描述信息,和异常的栈帧追踪。

异常名:告诉我们是什么异常,通常用类名表示,比如IOException,这是IO异常。

描述信息:则是该异常的具体描述,是如何发生的,比如网络连接失败等等。

异常的栈帧追踪:这记录了异常发生的代码位置。一般从main函数开始,记录每一步函数调用,一直到最后导致异常的代码。

public class Throwable implements Serializable {
    ...
     private String detailMessage;        /** 异常的具体细节*/

    /** 构造一个空的StackTrace数组 stackTrace  */
    private static final StackTraceElement[] UNASSIGNED_STACK = new StackTraceElement[0];
    private StackTraceElement[] stackTrace = UNASSIGNED_STACK;         /** 栈帧追踪 */

    ...
    /** 字符串s则是异常名,输出时直接输出异常的类型。比如IOException*/
    public String toString() {
        String s = getClass().getName();
        String message = getLocalizedMessage();
        return (message != null) ? (s + ": " + message) : s;
    }
}

stackTrace,栈帧追踪,是我们在debug时,经常需要获取的信息。可以帮我们定位bug的位置。

那么Throwable是如何操作该成员属性的呢。(这学习源码之前,我最好奇的地方,傻傻地猜想是用数组记录了每一个方法调用,可是很显然,这样不科学)

首先交代:stackTrace是由本地方法获得的,相关的两个本地方法如下,万恶的native是吧,笔者认为是和JVM有关。

    /** 本地方法,用于跟踪异常的栈帧 */
    native int getStackTraceDepth();      /** 获得追踪数组的深度 */
    native StackTraceElement getStackTraceElement(int index);  /** 获得下标为index的栈帧信息*/

具体如何使用这两个本地方法或者stackTrace,这个我们还是可以从源码中看到的。

getStackTrace():是一个公开的接口,给外界调用。

getOurStackTrace(): 则是真正的执行方法。先是查看stackTrace是否为UNASSIGNED_STACK(初始状态),或者是否为空,并且有追踪标志。如果满足,则可以调用本地方法得到数组深度depth,来分配stackTrace的空间,并将数组成员一个一个写入stackTrace中。

    /**
     *  获取当前状态异常的信息,
     *  获取栈上面的异常信息,(通过本地方法得到)
     *  遍历每一个异常信息,
     *  赋值给stackTrace进行保存
     * */
    public StackTraceElement[] getStackTrace() {
        return getOurStackTrace().clone();
    }

    private synchronized StackTraceElement[] getOurStackTrace() {
        if (stackTrace == UNASSIGNED_STACK ||
            (stackTrace == null && backtrace != null) /* Out of protocol state */) {
            int depth = getStackTraceDepth();             /** 通过本地方法得到栈帧数组的深度 depth*/
            stackTrace = new StackTraceElement[depth];    /** 初始化一个长度为depth的栈帧数组*/
            for (int i=0; i < depth; i++)
                stackTrace[i] = getStackTraceElement(i);  /** 得到本地方法中的栈异常清空,复值给stackTrace[i]*/
        } else if (stackTrace == null) {
            return UNASSIGNED_STACK;
        }
        return stackTrace;                               /** 返回栈帧数组 */
    }

我们在遇到异常时,经常需要将异常的栈帧追踪打印出来,使用printStackTrace()。该方法,可以设置一个输出流参数,比如System.out。将异常输出到控制台上,也可以缺省,在缺省的情况下,默认输出路径为System.err。具体代码如下:

    /**************************************************
     *  打印异常栈,从方法调用出直到异常抛出处
     *  可选参数:PrintStream s ;PrintWriter s。一个输出流。
     *  默认输出到System.err
     **************************************************/
    public void printStackTrace() {
        printStackTrace(System.err);
    }
    public void printStackTrace(PrintStream s) {
        printStackTrace(new WrappedPrintStream(s));
    }
    /** 打印异常信息 */
    private void printStackTrace(PrintStreamOrWriter s) {
        // Guard against malicious overrides of Throwable.equals by
        // using a Set with identity equality semantics.
        Set<Throwable> dejaVu =
            Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());
        dejaVu.add(this);

        synchronized (s.lock()) {
            // Print our stack trace
            s.println(this);
            StackTraceElement[] trace = getOurStackTrace();     /** 打印栈帧情况*/
            for (StackTraceElement traceElement : trace)
                s.println("\tat " + traceElement);

            // Print suppressed exceptions, if any
            for (Throwable se : getSuppressed())               /** 打印被压抑的异常(as try) */
                se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION, "\t", dejaVu);

            // Print cause, if any
            Throwable ourCause = getCause();                    /** 打印异常链*/
            if (ourCause != null)
                ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, "", dejaVu);
        }
    }

在这里,有一个getSupressed(),该方法是获得被压抑的异常。被压抑的异常在笔者理解中是在try()catch(){}中捕获的异常。

与压抑异常相对应的就是RuntimeException,该异常是运行时由于代码问题而产生的异常(比如数组越界等等,也称不受检查异常,即不需要我们进行检查、捕获,程序直接会抛出异常,并终止程序)

异常链。则可以理解为一个异常构成的链表。其实就是在处理某个异常A时,导致了另一个异常B的产生。那么B异常的成员变量cause则为异常A。

此外还有一个较为常用的方法。 fillInStackTrace()。该方法顾名思义就是将跟踪栈帧进行清空。底层实现也是本地方法。常用于将异常抛给上级处理,如调用该方法,则会重新设置栈帧跟踪。但是,描述信息和异常名不变。

    /**
     *  清空本异常之前的栈帧跟踪,通常用于向上级异常处理进行 抛出异常,之后调用
     *  保留原异常的名称和描述
     *  从抛出异常处重新设置栈帧跟踪。
     *
     * */
    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);

猜你喜欢

转载自blog.csdn.net/m0_37128231/article/details/85247261