Java异常体系

张贤 2020年03月16日 146次浏览

Java 从诞生之初就提供了完善的异常处理机制,大大降低了编写和维护可靠程序的门槛。Java 的异常处理机制主要回答了 3 个问题:

  • What:异常类型回答了什么被抛出
  • Where:异常堆栈跟踪回答了在哪里被抛出
  • Why:异常信息回答了为什么被抛出

Java异常体系类图


Error 以及 Exception

Error 和 Exception 的区别

  • Error:程序无法处理的系统错误,编译器不做检查,这种情况应该终止程序
  • Exception:程序可以处理的异常,捕获后可以恢复,应尽可能在程序内处理这种异常

Exception 的分类:

  • RuntimeException:这类异常是不可预知的,程序应该通过各种判断来自行避免
  • 可预知的异常,编译器强制必须校验的异常。如不哦程序不处理,则编译不通过
    不要抛出异常的父类来粗化异常

从责任角度看:

  • Error 属于 JVM 需要承担的责任
  • RuntimeException 属于程序应该负担的责任
  • Checked Exception 可检查异常是 Java 编译器应该承担的责任

常见 Error 以及 Exception

  • NullPointException:空指针引用异常
  • ClassCastException:类型强制转换异常
  • IllegalArgumentException:传递非法参数异常
  • IndexOutOfBoundsException:下标越界异常
  • NumberFormatException:数字格式异常

Checked Exception

  • ClassNotFoundException:找不到指定 Class 的异常
  • IOException:IO 操作异常

Error

  • NoClassDefFoundError:找不到 Class 定义的异常
    • 类依赖的 Class 或者 jar 不存在
    • 类文件存在,但同一个类被多个类加载器重复加载,存在于不同的域中
    • 大小写问题:javac 编译是无视大小写的,很有可能编译出来的 Class 文件与想要的不一样
  • StackOverflowError:深递归导致栈被耗尽而抛出的异常
  • OutOfMemoryError:内存溢出异常

下面是一个例子:

public class ExceptionAndError {
    private void throwError() {
        throw new StackOverflowError();
    }

    private void throwRuntimeException() {
        throw new RuntimeException();
    }

    private void throwCheckedException() {
        throw new IOException();
    }
}

上面的代码里,第三个方法体throwCheckedException()内的异常属于 CheckedException(可检查异常),编译器会强制要求捕获该异常,否则编译不能通过。
可以使用try-catch把异常捕获,在catch里面增加相应的处理逻辑,具体的处理逻辑需要依据业务需要而定,一般的原则是理解这种Exception的原因,并结合实际业务处理,如下所示。

private void throwCheckedException() {
    try {
        throw new IOException();
    } catch (IOException e) {
        //在这里根据业务需要处理异常
        e.printStackTrace();
    }
}

在实际场景中,一个项目是由很多模块组成的。假设上面的代码是模块 A 的,如果 B 调用了 A 的方法产生了异常,那么异常不应该由 A 使用try-catch捕获,因为A 并不了解 B 的业务,因此 A 难以根据 B 的业务来处理Exception,因此最佳实践是 A 把异常继续往上抛出,由上层调用者来处理异常,如下所示。

//最佳实践:把异常往上抛出(并且不要抛出异常的父类,这样会粗化异常,丢失异常细节信息),由调用者来处理异常
private void throwCheckedException() throws IOException {
    throw new IOException();
}

在引发异常时,会在堆上创建异常对象,对象包含异常类型和程序运行状态等异常信息。然后中断当前正在执行的流程,由运行时系统去寻找合适的异常处理器。在catch代码块 return 之前,会先执行 finally 代码块的语句,所以 finally 里的 return 会先于 catch 代码块的 return。

捕获异常时不要捕获异常的父类,会给团队中其他同事掩盖具体的异常信息,并且可能捕获到不希望捕获的异常。
不要生吞异常,如果不在第一时间抛出异常,程序会以不可控的方式结束,并且难以定位出错的地方

Java 异常的处理原则

  • 具体明确:抛出的异常应该能够通过异常类名和 message 准确说明异常的类型和产生异常的原因
  • 提早抛出:应尽可能早地发现并抛出异常,便于精确定位问题
  • 延迟捕获:异常的捕获和处理应该尽可能延迟,让掌握更多信息的作用域处理异常

下面来对比if-elsetry-catch的性能

public class ExceptionPerformance {
    public static void testException(String[] array) {
        try {
            System.out.println(array[0]);
        } catch (Exception e) {
            System.out.println("array can not be null");
        }
    }

    public static void testIf(String[] array) {
        if (array != null) {
            System.out.println(array[0]);
        } else {
            System.out.println("array can not be null");
        }
    }

    public static void main(String[] args) {
        long start = System.nanoTime();
        testIf(null);
        long end = System.nanoTime();
        System.out.println("if-else handle time:" + (end - start));

        start = System.nanoTime();
        testException(null);
        end = System.nanoTime();
        System.out.println("Exception handle time:" + (end - start));
    }
}

上面代码对比了if-elsetry-catch的耗时,一般来说if-else的耗时总会比try-catch的耗时短

Java 异常处理消耗性能的地方:

  • try-catch块影响 JVM 的优化,影响了指令重排序带来的优化
  • 异常对象实例需要保存堆栈快照等信息,开销较大
    所以建议仅捕获可能出现异常的必要的代码段,不要用 try-catch 包住整个代码段,也不要用 try-catch 来控制代码流程,因为 try-catch 远没有 if-else 和 switch 的效率高

高效主流的异常处理框架

  • 设计一个通用的继承自 RuntimeException 的异常 AppException 来统一处理
  • 其余异常都统一转译为上述异常 AppException
  • 在 catch 之后,抛出上述异常 AppException 的子类,并提供足以定位异常位置的业务友好的信息,而非之前一大堆无关的堆栈信息
  • 最后由前端接收 AppException 做统一处理