资讯专栏INFORMATION COLUMN

浅析Java异常处理机制

NSFish / 2219人阅读

摘要:关于异常处理的文章已有相当的篇幅,本文简单总结了的异常处理机制,并结合代码分析了一些异常处理的最佳实践,对异常的性能开销进行了简单分析。是程序正常运行中,可以预料的意外情况,应该被捕获并进行相应处理。

关于异常处理的文章已有相当的篇幅,本文简单总结了Java的异常处理机制,并结合代码分析了一些异常处理的最佳实践,对异常的性能开销进行了简单分析。
博客另一篇文章《[译]Java异常处理的最佳实践》也是关于异常处理的一篇不错的文章。

请思考: 对比 ExceptionError ,二者有何区别? 另外,运行时异常和一般异常有什么区别?

Exception 和 Error 的区别

首先,要明确的是 ExceptionError 都继承自 Throwable 类,Java中只有 Throwable 类型的实例才可以被抛出 (throws) 或 捕获 (catch) ,它是异常处理机制的基本组成类型。

Exception 是程序正常运行中,可以预料的意外情况,应该被捕获并进行相应处理。
Error 是指在正常情况下,不太可能出现的情况,绝大多数的 Error 都会导致程序(比如 JVM 自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的如 OutOfMemoryError等,都是 Error 的子类。

Exception 又分为检查型 (checked) 和 非检查型 (unchecked) 异常,检查型异常必须在源代码里显式的进行捕获处理,这是编译期检查的一部分。

非检查型异常(unchecked exception) 就是所谓的运行时异常,如 NullPointerExceptionArrayIndexOutOfBoundsException 等,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。

Throwable、Exception、Error 的设计和分类 内置异常类

下图展示了Java中异常类继承关系

java.lang 中定义了一些异常类,这里只列举其中常见的一部分,详细查阅 java.lang.Error、java.lang.Exception

Error:

LinkageError:

VirtualMachineError:虚拟机错误。用于指示虚拟机被破坏或者继续执行操作所需的资源不足的情况。

OutOfMemoryError: 内存溢出错误

StackOverflowError:栈溢出错误

Exception

检查型异常 (checked exception)

IOException

ClassNotFoundException

InstantiationException

SQLException

非检查型异常 (unchecked exception)

RuntimeException

NullPointerException

ClassCastException

SecurityException

ArithmeticException

IndexOutOfBoundsException

还有一个经典的题目: NoClassDefFoundErrorClassNotFoundException 有什么区别?

异常方法

下面是 Throwable 类的主要方法:(java.lang.Throwable)

public String getMessage() :返回关于发生的异常的详细信息

public Throwable getCause():返回一个Throwable 对象代表异常原因

public void printStackTrace():打印toString()结果和栈层次到System.err,即错误输出流。

public String toString():Returns a short description of this throwable.

捕获、抛出异常 try-catch-finally

使用trycatch 关键字可以捕获异常。
可以在 try 语句后面添加任意数量的 catch 块来捕获不同的异常。如果保护代码中发生异常,异常被抛给第一个 catch 块,如果匹配,它在这里就会被捕获。如果不匹配,它会被传递给第二个 catch 块。如此,直到异常被捕获或者通过所有的 catch 块。
无论是否发生异常,finally 代码块中的代码总会被执行。在 finally 代码块中,可以做一些资源回收工作,如关闭JDBC连接。

try{
    // code 
}catch( 异常类型1 ex ){
    //..
}catch( 异常类型2 ex){
    //..
}catch( 异常类型3 ex ){
    //..
}finally{
   //..
}
throw、throws

throw 的作用是抛出一个异常,无论它是新实例化的还是刚捕获到的。
throws 是方法可能抛出异常的声明。使用 throws 关键字声明的方法表示此方法不处理异常,而交给方法调用处进行处理,一个方法可以声明抛出多个异常。
例如,下面的方法声明抛出 RemoteExceptionInsufficientFundsException

public class className
{
   public void withdraw(double amount) throws RemoteException,
                              InsufficientFundsException
   {
       // Method implementation
       if(..)
           throw new RemoteException();
       else
           throw new InsufficientFundsException();
   }
   //Remainder of class definition
}
try-with-resources 和 multiple catch

从Java 7开始提供了两个有用的特性:try-with-resourcesmultiple catch
try-with-resources 将 try-catch-finally 简化为 try-catch,这其实是一种语法糖,在编译时会转化为 try-catch-finally 语句。自动按照约定俗成 close 那些扩展了 AutoCloseable 或者 Closeable 的对象,从而替代了finally中关闭资源的功能。以下代码用try-with-resources 自动关闭 java.sql.Statement

public static void viewTable(Connection con) throws SQLException {

    String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES";

    try (Statement stmt = con.createStatement()) { // Try-with-resources
        ResultSet rs = stmt.executeQuery(query);

        while (rs.next()) {
            String coffeeName = rs.getString("COF_NAME");
            int supplierID = rs.getInt("SUP_ID");
            float price = rs.getFloat("PRICE");
            int sales = rs.getInt("SALES");
            int total = rs.getInt("TOTAL");

            System.out.println(coffeeName + ", " + supplierID + ", " + 
                               price + ", " + sales + ", " + total);
        }
    } catch (SQLException e) {
        JDBCTutorialUtilities.printSQLException(e);
    }
}

值得注意的是,异常抛出机制发生了变化。在过去的 try-catch-finally 结构中,如果 try 块没有发生异常时,直接执行finally块。如果try 块发生异常,catch 块捕捉,然后执行 finally 块。

但是在 try-with-resources 结构中,不论 try 中是否有异常,都会首先自动执行 close 方法,然后才判断是否进入 catch块。分两种情况讨论:

try 没有发生异常,自动调用close方法,如果发生异常,catch 块捕捉并处理异常。

try 发生异常,然后自动调用 close 方法,如果 close 也发生异常,catch 块只会捕捉 try 块抛出的异常,close 方法的异常会在 catch 中被压制,但是你可以在 catch 块中,用Throwable.getSuppressed 方法来获取到压制异常的数组。

再来看看multiple catch ,当我们需要同时捕获多个异常,但是对这些异常处理的代码是相同的。比如:

try {
    execute(); //exception might be thrown
} catch (IOException ex) {
    LOGGER.error(ex);
    throw new SpecialException();
} catch (SQLException ex) {
    LOGGER.error(ex);
    throw new SpecialException();
} 

使用 multiple catch 可以把代码写成下面这样:

try {
    execute(); //exception might be thrown
} catch (IOException | SQLExceptionex ex) {// Multiple catch
    LOGGER.log(ex);
    throw new SpecialException();
}

这里需要注意的是,上面代码中 ex是隐式的 final 不可以在catch 块中改变ex。

自定义异常

有的时候,我们会根据需要自定义异常。自定义的所有异常都必须是 Throwable 的子类,如果是检查型异常,则继承 Exception 类。如果自定义的是运行时异常,则继承 RuntimeException。这个时候除了保证提供足够的信息,还有两点需要考虑:

是否需要定义成 Checked Exception,这种类型设计的初衷是为了从异常情况恢复。

在保证诊断信息足够的同时,也要考虑避免包含敏感信息,因为那样可能导致潜在的安全问题。例如java.net.ConnectException的出错信息是"Connection refused(Connection refused)",而不包含具体的机器名、IP、端口等,一个重要考量就是信息安全。类似的情况在日志中也有,比如,用户数据一般是不可以输出到日志里面的。

异常处理的最佳实践

看下面代码,有哪些不当之处?

try {
    // …
    Thread.sleep(1000L);
} catch (Exception e) {
} 

以上代码虽短,但已经违反了异常处理的两个基本原则。
第一,尽量不要捕获顶层的Exception,而是应该捕获特定异常。 在这里是 Thread.sleep() 抛出的 InterruptedException。我们希望自己的代码在出现异常时能够尽量给出详细的异常信息,而Exception恰恰隐藏了我们的目的,另外我们也要保证程序不会捕获到我们不希望捕获的异常,而上边的代码将捕获所有的异常,包括 unchecked exception ,比如,你可能更希望
RuntimeException 被扩散出来,而不是被捕获。进一步讲,尽量不要捕获 Throwable 或者 Error,这样很难保证我们能够正确处理程序 OutOfMemoryError
第二,不要生吞(swallow)异常 ,这是异常处理中要特别注意的事情,因为很可能会导致非常难以诊断的诡异情况。当try块发生 checked exception 时,我们应当采取一些补救措施。如果 checked exception 没有任何意义,可以将其转化为 unchecked exception 再重新抛出。千万不要用一个空的 catch 块捕获来忽略它,程序可能在后续代码以不可控的方式结束,没有人能够轻易判断究竟是哪里抛出了异常,以及是什么原因产生了异常。

try {
    // …
} catch (IOException e) {
    e.printStackTrace();
}

这段在实验中没问题的代码通常在产品代码中不允许这样处理。
查看printStackTrace()文档开头就是“Prints this throwable and its backtrace to the standard error stream”,问题就在这,在稍微复杂一点的生产系统中,标准出错(STERR)不是个合适的输出选项,因为很难判断出到底输出到哪里去了。尤其是对于分布式系统,如果发生异常,但是无法找到堆栈轨迹(stacktrace),这纯属是为诊断设置障碍。所以,最好使用产品日志,详细地输出到日志系统里。

Throw early, catch late 原则

This is probably the most famous principle about Exception handling. It basically says that you should throw an exception as soon as you can, and catch it late as much as possible. You should wait until you have all the information to handle it properly.

This principle implicitly says that you will be more likely to throw it in the low-level methods, where you will be checking if single values are null or not appropriate. And you will be making the exception climb the stack trace for quite several levels until you reach a sufficient level of abstraction to be able to handle the problem.
看下面的代码段:

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

上段代码中如果 fileName 为 null,那么程序就会抛出 NullPointerException,但是由于没有第一时间暴露出问题,堆栈信息可能非常令人费解,往往需要相对复杂的定位。在发现问题的时候,第一时间抛出,能够更加清晰地反映问题。

修改一下上面的代码,让问题 “throw early”,对应的异常信息就非常直观了。

public void readPreferences(String filename) {
    Objects. requireNonNull(filename);   // throw NullPointerException
    //...perform other operations...
    InputStream in = new FileInputStream(filename);
    //...read the preferences file...
}

上面这段代码使用了Objects.requireNonNull()方法,下面是它在java.util.Objects里的具体实现:

public static  T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;
    }

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

异常处理机制的性能开销

从性能角度审视一下Java的异常处理机制,有两个可能会相对昂贵的地方:

try-catch 代码段会产生额外的性能开销,换个角度说,它往往会影响JVM对代码进行优化,所以建议仅捕获有必要的代码段,尽量不要一个大的 try 包住整段的代码;更不要利用异常控制代码流程,这远比我们通常意义上的条件语句(if/else、switch)要低效。

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

所以,对于部分追求极致性能的底层类库,有种方式是尝试创建不进行栈快照的Exception。另外,当我们的服务出现反应变慢、吞吐量下降的时候,检查发生最频繁的 Exception 也是一种思路。

参考文章:

Java 异常处理 - runoob.com

Designing with exceptions :Guidelines and tips on when and how to use exceptions

Exception和Error有什么区别? - Java核心技术36讲

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/72623.html

相关文章

  • 浅析微信支付:支付结果通知

    摘要:本文是浅析微信支付系列文章的第六篇,主要讲解支付成功后,微信回调商户支付结果通知的处理。微信支付支付回调接口该链接是通过统一下单中提交的参数设置,如果链接无法访问,商户将无法接收到微信通知。 本文是【浅析微信支付】系列文章的第六篇,主要讲解支付成功后,微信回调商户支付结果通知的处理。 浅析微信支付系列已经更新五篇了哟~,没有看过的朋友们可以看一下哦。 浅析微信支付:统一下单接口 浅析...

    Dean 评论0 收藏0
  • 浅析php中的异常与错误

    摘要:异常与错误异常是指程序运行中不符合预期情况以及与正常流程不同的状况。在中主要的错误等级如下最低级别的错误,表示不推荐不建议。小结中错误和异常是两个不同的概念,这种设计根本上导致了的异常和错误与其它语言相异。中,异常时错误唯一的报告方式。 异常与错误 异常是指程序运行中不符合预期情况以及与正常流程不同的状况。错误则属于自身问题,是一种非法语法或者环境问题导致的、让编译器无法通过检查设置无...

    Leck1e 评论0 收藏0
  • GC(@广告位出售)垃圾回收机制浅析与理解

    摘要:广告位出售垃圾回收机制浅析与理解对垃圾回收进行分析前,我们先来了解一些基本概念基本概念内存管理内存管理对于编程语言至关重要。里面的变量通常是局部变量函数参数等。 GC(@广告位出售)垃圾回收机制: 浅析与理解 对垃圾回收进行分析前,我们先来了解一些基本概念 基本概念 内存管理:内存管理对于编程语言至关重要。汇编允许你操作所有东西,或者说要求你必须全权处理所有细节更合适。C 语言中虽然...

    songjz 评论0 收藏0
  • GC(@广告位出售)垃圾回收机制浅析与理解

    摘要:广告位出售垃圾回收机制浅析与理解对垃圾回收进行分析前,我们先来了解一些基本概念基本概念内存管理内存管理对于编程语言至关重要。里面的变量通常是局部变量函数参数等。 GC(@广告位出售)垃圾回收机制: 浅析与理解 对垃圾回收进行分析前,我们先来了解一些基本概念 基本概念 内存管理:内存管理对于编程语言至关重要。汇编允许你操作所有东西,或者说要求你必须全权处理所有细节更合适。C 语言中虽然...

    xioqua 评论0 收藏0
  • 浅析 Python 的类、继承和多态

    摘要:类的定义假如要定义一个类,表示二维的坐标点最最基本的就是方法,相当于的构造函数。严格来讲,并不支持多态。静态类型的缺失,让很难实现那样严格的多态检查机制。有时候,需要在子类中调用父类的方法。 类的定义 假如要定义一个类 Point,表示二维的坐标点: # point.py class Point: def __init__(self, x=0, y=0): se...

    shadajin 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<