Java核心技术36讲--Exception和Error的区别

Java异常处理机制中的Exception和Error

Exception和Error都继承了Throwable类,在Java中只有Throwable类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型,Exception 和 Error 体现了 Java 平台设计者对不同异常情况的分类

Exception的含义

Exception是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应的处理。

Exception又可分为可检查异常不检查异常

  • 可检查异常在源代码里必须显式地进行捕获处理,这是编译器检查的一部分

  • 不检查异常就是所谓的运行时异常,例如NullPointerException、ArrayIndexOutOfBoundException,通常是可以通过编码避免的逻辑错误,具体需要根据实际情况来判断是否需要捕获,并不会在编译期强制要求

Error的含义

Error是指在正常情况下,不大可能出现的情况,绝大部分的Error都会使应用程序处于非正常的、不可恢复的状态。既然是非正常情况,所以不便于也不用捕获。

Java异常处理的常见考点

  • 需要理解Throwable、Exception、Error的设计和分类,掌握应用最为广泛的子类,以及学会如何自定义异常。

    Throwable及其子类图

  • 需要理解Java语言中操作Throwable的元素和实践。掌握最基本的语法是必须的,如try-catch-finally块,throw、throws关键字等。与此同时,还要学会懂得如何处理典型场景

    例如先看一下下面这段代码

    1
    2
    3
    4
    5
    6
    7
    try {
    // 业务代码
    // …
    Thread.sleep(1000L);
    } catch (Exception e) {
    // Ignore it
    }

    这段代码虽然很短,但是已经违反了异常处理的两个基本原则

    • 尽量不要捕获类似Exception这样的通用异常,而是应该捕获特定异常。在这里是 Thread.sleep() 抛出的应该是InterruptedException。 应该让自己的代码能够直观地体现出尽量多的信息,而泛泛的Exception之类,恰恰隐藏了我们的目的 。另外,我们也要保证程序不会捕获到我们不希望捕获的异常,例如运行时异常。进一步讲,除非深思熟虑了,否则不要捕获 Throwable或者Error,这样很难保证我们能够正确程序处理 OutOfMemoryError 。

    • 不要生吞(swallow)异常。这是异常处理中要特别注意的事情,因为很可能会导致非常难以诊断的诡异情况。生吞异常,往往是基于假设这段代码可能不会发生,或者感觉忽略异常是无所谓的,但是千万不要在产品代码做这种假设!

      如果我们不把异常抛出来,或者也没有输出到日志(Logger)之类,程序可能在后续代码以不可控的方式结束。没人能够轻易判断究竟是哪里抛出了异常,以及是什么原因产生了异常。

      再来看看第二段代码

    1
    2
    3
    4
    5
    6
    try {
    // 业务代码
    // …
    } catch (IOException e) {
    e.printStackTrace();
    }

    这段代码作为一段实验代码,它是没有任何问题的,但是在产品代码中,通常都不允许这样处理。在稍微复杂一点的生产系统中,标准出错(STERR)不是个合适的输出选项,因为你很难判断出到底输出到哪里去了 ,所以,最好使用产品日志,详细地输出到日志系统里。

    最后看下下面两段代码

    1
    2
    3
    4
    5
    public void readPreferences(String fileName){
    //...perform operations...
    InputStream in = new FileInputStream(fileName);
    //...read the preferences file...
    }

    在这一段代码中,如果fileName是 null,那么程序就会抛出 NullPointerException,但是由于没有第一时间暴露出问题,堆栈信息可能非常令人费解,往往需要相对复杂的定位。 因此,可以将上述代码改为如下形式

    1
    2
    3
    4
    5
    6
    public void readPreferences(String filename) {
    Objects. requireNonNull(filename);
    //...perform other operations...
    InputStream in = new FileInputStream(filename);
    //...read the preferences file...
    }
    • 应该遵循”Throw early, catch late “原则

      “Throw early”,在发现问题的时候,第一时间抛出,这样才能够更加清晰地反映问题。

      “catch late”,在捕获异常后的处理上,最差的处理方式,就是前面提到的“生吞异常”,本质上其实是掩盖问题。如果实在不知道如何处理,可以选择保留原有异常的 cause 信息,直接再抛出或者构建新的异常抛出去。在更高层面,因为有了清晰的(业务)逻辑,往往会更清楚合适的处理方式是什么。

自定义异常的注意事项

  • 考虑是否需要定义成Checked Exception,因为这种类型设计的初衷更是为了从异常情况恢复,作为异常设计者,我们往往有充足信息进行分类
  • 在保证诊断信息足够的同时,也要考虑避免包含敏感信息,因为那样可能导致潜在的安全问题。如果我们看 Java 的标准类库,你可能注意到类似 java.net.ConnectException,出错信息是类似“ Connection refused (Connection refused)”,而不包含具体的机器名、IP、端口等,一个重要考量就是信息安全。类似的情况在日志中也有,比如,用户数据一般是不可以输出到日志里面的

Java 的异常处理机制对性能的影响

  • try-catch代码段会产生额外的性能开销,或者换个角度说,它往往会影响JVM对代码进行优化

    • 建议仅捕获有必要的代码段,尽量不要一个大的try包住整段的代码。

    • 不要利用异常控制代码流程,应该使用if,else,switch等。

  • Java每实例化一个Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁,这个开销可就不能被忽略了

    当我们的服务出现反应变慢、吞吐量下降的时候,检查发生最频繁的Exception也是一种思路 。

总结

  • Exception和Error的异同

    相同点:Exception和Error都继承了Throwable类。

    不同点Exception是程序正常运行中,可以预料的意外情况,可以也应该被捕获并进行相应的处理;Error是指在正常情况下,不大可能出现的情况,绝大部分的Error都会使应用程序处于非正常的、不可恢复的状态,不用也不能捕获。

  • 异常处理应当遵循的以下几个原则

    尽量不要捕获类似Exception这样的通用异常,而是应该捕获特定异常。

    不要生吞(swallow)异常,不要简单使用printStackTrace()。

    应该遵循”Throw early, catch late”原则。

  • 自定义异常需要注意的事项

    是否需要定义成Checked Exception。

    在保证诊断信息足够的同时,也要考虑避免包含敏感信息,因为那样可能导致潜在的安全问题。

  • Java 的异常处理机制对性能的影响

    try-catch代码段会产生额外的性能开销,建议仅捕获有必要的代码段,尽量不要一个大的try包住整段的代码,也不要利用异常控制代码流程,应该使用if,else,switch等。

    Java异常处理机制每实例化一个Exception实例,会产生当前快照,产生一定的开销,当达到一定程度时,开销产生的影响不可忽略。