美文网首页Java
重学Java异常体系

重学Java异常体系

作者: 消失er | 来源:发表于2023-04-14 19:48 被阅读0次

    Java异常类的层次结构

    Throwable是所有异常类的基类。
    Throwable包含了其线程创建时线程执行堆栈的快照,它提供了 printStackTrace() 等接口用于获取堆栈跟踪数据等信息。

    Throwable 分为两种:Error和Exception。
    1.Error:Error是系统级别的错误,无法通过程序处理;此类错误一般表示代码运行时 JVM 出现问题,例如OutOfMemoryError、StackOverflowError、VirtualMachineError等。

    2.Exception是应用程序级别的异常,可以通过程序处理;通常会分为Checked Exception和Unchecked Exception。

    • Checked Exception,也称为受检异常,在编译时就需要进行处理,否则会报编译错误,例如IOException、SQLException等。
    • Unchecked Exception,也称为非受检异常,在运行时才会抛出,不需要进行强制处理,例如NullPointerException、ArrayIndexOutOfBoundsException等。

    受检异常 vs 非受检异常

    受检异常
    • 本质是应用程序执行过程中可能遇到的异常情形,必须由当前调用方处理;即调用方必须捕获并处理被调方法签名上声明的异常;
    • 这类异常,在一定程度上它的发生是可以预计的(例如从配置文件读取配置项,在程序运行之前就可以预计到文件可能不存在),而一旦发生异常状况,就必须采取某种方式进行处理。受检异常在编译期必须显式地被处理,否则编译器将无法通过代码。

    在java.lang.Exception类注释上,说明了Exception及其子类(排除RuntimeException及其子类),都是checked exceptions受检异常。

    /**
     * The class Exception and its subclasses are a form of
     * {@code Throwable} that indicates conditions that a reasonable
     * application might want to catch.
     *
     * The class Exception and any subclasses that are not also
     * subclasses of {@link RuntimeException} are checked
     * exceptions  Checked exceptions need to be declared in a
     * method or constructor's {@code throws} clause if they can be thrown
     * by the execution of the method or constructor and propagate outside
     * the method or constructor boundary.
     */
    public class Exception extends Throwable {...}
    
    非受检异常
    • 在一定程度上它的发生是不可预计的,只有实际运行到那时才可能发生;比如访问数组越界,程序运行之前是无法预计的。JDK将这类异常命名为RuntimeException运行时异常也恰如其分。
    • 这类异常的发生通常是由于程序 bug 所致,应该尽量通过预先检查进行规避;比如在面对可能抛NullPointerException的地方时主动判断是不是null 并处理、循环处理时要检查下标边界防止IndexOutOfBounds。

    在java.lang.RuntimeException类注释上,说明了RuntimeException及其子类,都是Unchecked Exception非受检异常。

    /**
     * RuntimeException is the superclass of those
     * exceptions that can be thrown during the normal operation of the
     * Java Virtual Machine.
     *
     * RuntimeException and its subclasses are unchecked exceptions.  
     *  Unchecked exceptions do not need to be
     * declared in a method or constructor's {@code throws} clause if they
     * can be thrown by the execution of the method or constructor and
     * propagate outside the method or constructor boundary.
     *
     * @author  Frank Yellin
     * @jls 11.2 Compile-Time Checking of Exceptions
     * @since   JDK1.0
     */
    public class RuntimeException extends Exception {...}
    

    Java异常处理机制

    Java 的异常处理,主要是通过 5 个关键字来实现:try、catch、throw、throws 和 finally。
    通过捕获和处理异常,能够在程序出现异常情况时提供更好的容错机制,保证程序的稳定性和可靠性。

    throws与throw

    • throws出现在方法签名的声明中,表示该方法可能会抛出的异常,然后交给上层调用它的方法程序处理,允许throws后面跟着多个异常类型;一般用在程序运行之前就预计可能发生的异常场景上,让调用方显示处理。
    • throw只会出现在方法体中,当方法在执行过程中遇到异常情况时,将异常信息封装为异常对象,然后throw出去。

    throw 关键字的作用是抛出一个异常,但是它的作用不仅仅局限于抛出异常。在一些情况下,throw 关键字还可以实现异常类型的转换。这是因为 Java 中的异常继承关系,子类异常对象可以转换为父类异常对象,而 throw 关键字可以将子类异常对象强制转换为父类异常对象。

    public void doSomething() throws Exception {
        try {
            // do something
        } catch (ChildException e) { // 子类异常
            throw (Exception)e; // 异常类型转换为父类异常
        }
    }
    

    throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常对象。

    关于finally特殊问题和Java异常处理的最佳实践,后面用单独的文章讨论。

    覆盖父类方法时的异常声明

    从父类继承,并且子类重写/覆盖的方法签名,如何声明异常?
    子类重写父类方法的时候,如何确定异常抛出声明的类型。主要有4个原则:

    1.子类重写父类方法时,要抛出与父类一致的异常,或者不抛出异常

    2.子类重写父类方法时,子类抛出的异常不能超过父类的受检异常类型范围
    即如果父类的方法声明了受检异常T,则子类在重写该方法的时候声明的异常不能是 T的父类;只能是T及T的子类。

    3.子类重写的方法可以抛出任意非受检异常

    4.子类在重写父类的具有异常声明的方法的同时,又去实现了具有相同方法名称的接口且该接口中的方法也具有异常声明,则子类中的重写的方法,要么不抛出异常,要么抛出父类中方法声明异常与接口中方法声明的异常的交集。

    自定义异常

    在 Java 中,通常情况下已有的内置异常类能够满足应用的基础使用需求。但有些情况下,需要自定义异常来满足特别的需求。
    最常见的就是用精确的命名描述异常

    • 例如,某个特定case的异常,让它见名知意。当程序需要对异常情况进行更加具体和准确的描述时,可以自定义异常类。

    如何自定义异常就不多描述了,这里着重说一点关于自定义异常时需要注意的地方。

    重写fillInStackTrace方法

    Java 异常在程序运行时发生,会导致程序的执行流程被中断并进行一系列的异常处理操作,这会对程序的性能产生一定的影响。其中性能开销最大的是填充堆栈。
    默认情况下,是由Throwable的fillInStackTrace方法完成,它是一个native方法;作用是获取当前线程的堆栈信息,填充到跟踪元素中。

    public synchronized Throwable fillInStackTrace() {
        if (stackTrace != null || backtrace != null ) {// 如果没填充过……
            fillInStackTrace(0);                // 获取当前线程的堆栈跟踪信息
            stackTrace = UNASSIGNED_STACK;      // 填充堆栈跟踪信息
        }
        return this;// 如果已经填充过,直接返回当前异常对象
    }
    

    一般来说自定义的业务异常如果确实不需要stacktrace的话,可以覆写该方法,返回this,提高性能。

    @Override
    public synchronized Throwable fillInStackTrace() {
        // return super.fillInStackTrace();
        return this;
    }
    

    当前还有其他实现方式:

    • jvm参数控制栈深度,物理屏蔽;
    • logback日志框架控制堆栈的输出深度,逻辑屏蔽;
    • jvm本身对某些异常也做了优化,jvm有个参数OmitStackTraceInFastThrow(省略异常快速抛出), 如果检测到在代码里某个位置连续多次抛出同一类型异常的话,C2即时编译器会决定用Fast Throw方式来抛出异常,而异常Trace即详细的异常栈信息会被清空。

    相关文章

      网友评论

        本文标题:重学Java异常体系

        本文链接:https://www.haomeiwen.com/subject/pkstjdtx.html