关于异常处理的文章已有相当的篇幅,本文简单总结了Java的异常处理机制,并结合代码分析了一些异常处理的最佳实践,对异常的性能开销进行了简单分析。

博客另一篇文章《[译]Java异常处理的最佳实践》也是关于异常处理的一篇不错的文章。

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

Exception 和 Error 的区别

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

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

Error 是指在正常情况下,不太可能出现的情况,绝大多数的 Error 都会导致程序(比如 JVM 自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的如 OutOfMemoryError等,都是 Error 的子类。

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

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

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

还有一个经典的题目: NoClassDefFoundError 和 ClassNotFoundException 有什么区别?

异常方法

下面是 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

使用try 和 catch 关键字可以捕获异常。

可以在 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 关键字声明的方法表示此方法不处理异常,而交给方法调用处进行处理,一个方法可以声明抛出多个异常。

例如,下面的方法声明抛出 RemoteException 和 InsufficientFundsException:

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-resources 和 multiple 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提供两种处理异常的机制_浅析Java异常处理机制相关推荐

  1. java start打开cmd窗口并停住_浅析Java命令执行

    在使用java.lang.Runtime#exec()执行命令时,为何有时候命令前缀需要加cmd /c或者bash -c?今天就来一探究竟! Java执行命令的3种方法 首先了解下在Java中执行命令 ...

  2. java中两个xml文件内容拼接_比较Java中2个XML文档的最佳方法

    慕丝7291255 听起来像是XMLUnit的工作http://www.xmlunit.org/https://github.com/xmlunit例子:public class SomeTest e ...

  3. java中两种异常类型_Java中的三种异常类型

    java中两种异常类型 Errors are the bane of users and programmers alike. Developers obviously don't want thei ...

  4. java的两种运行方式Applet和Application你真的懂吗

    对两者的简介 他们是java的两种程序,能够独立运行的程序称为Java应用程序也包含我们正常写的java文件所生成的可执行程序(Application)其运行和普通的java文件相同.Java语言还有 ...

  5. java的两种比较器

    比较算法 日常生活中,如果想比较两个数的大小,可采用做差的方式,做差结果的正负可用来判断两个数的大小.假设A - B = C 若整数C > 0,说明 A > B ; 若整数C = 0,说明 ...

  6. Java中两种抛出异常的方式

    Java中两种抛出异常的方式 在Java中有两种抛出异常的方式,一种是throw,直接抛出异常,另一种是throws,间接抛出异常. 直接抛出异常是在方法中用关键字throw引发明确的异常.当thro ...

  7. Java多线程两种实现方式的对比

    Java多线程两种实现方式的对比 一种,直接继承Thread类 一种,实现Thread类的Runnable接口 两种方式的区别 比如,售票厅有四个窗口,可以发售某日某次列出的100张车票,此时,100 ...

  8. 十进制转二进制,用java的两种基本方法,适合新手

    十进制转二进制,用java的两种基本方法,适合新手 1.String字符串拼接法 package cn.sxt;import java.util.Scanner;/*** 6. 从键盘输入某个十进制整 ...

  9. Java实现两种方式 RSA签名, RSA签名校验

    Java实现两种方式 RSA签名, RSA签名校验 通过 .keystore密钥文件实现 生成密钥文件 test2.keystore 相关使用 通过密钥生成器实现 Byte数据转换成 Hex字符串 相 ...

最新文章

  1. vue中轻松搞掂鼠标气泡框提示框tip跟随
  2. 第二章 在HTML中使用JavaScript
  3. instanceof 的作用
  4. JNI/NDK开发指南(三)——JNI数据类型及与Java数据类型的映射关系
  5. 小程序---模板的引用与使用
  6. graylog2 架构--转载
  7. windows下的MySql实现读写分离
  8. 复杂网络下多码率视频流切换关键技术
  9. ghostblog主题_读Ghost博客源码与自定义Ghost博客主题
  10. linux开机脚本文件下载,linux 开机启动脚本
  11. Css学习总结(1)——20个很有用的CSS技巧
  12. java核心面试_前100多个核心Java面试问题
  13. 蓝牙芯片 csr8645 和 qcc3005 哪个比较
  14. 服务器显示灰色怎么办,服务器远程桌面显示灰色
  15. eTS UI开发学习
  16. 迅捷路由器造成计算机无法上网,迅捷无线路由器设置好却不能上网
  17. 《勋伯格和声学》读书笔记(八):转调
  18. Linkflow CDP亮相GDMS全球数字营销峰会
  19. Kubernetes Kubelete 报错 ctr: failed to create shim task: OCI runtime create failed
  20. 利用CIBERSORT免疫细胞类群分析详细教程

热门文章

  1. 工科学生懂艺术,魔鬼神仙挡不住
  2. 利用 RDA5807的RSSI测量RF强度
  3. 安装 express4 linux,nodejs+express4.0+mongodb安装方法 for Linux, Mac
  4. 地址设置nginx负载均衡_nginx负载均衡配置实例
  5. 丛林谜题JAVA_丛林王座图文全剧情流程攻略_全谜题解答通关流程_3DM单机
  6. 极大似然估计_干货|一文理解极大似然估计
  7. java日期存入数据库_怎样在Java中将日期转化插入到数据库
  8. vue源码解析pdf_Vue源码全面解析八 callHook函数(触发生命周期函数)
  9. 减少亚稳态导致错误,提高系统的MTBF
  10. Virtex-6系列FPGA的CLB