Java异常处理体系详解

Java异常处理体系详解

异常指程序中不期而至的各种状况,如:文件找不到、网络连接失败、非法参数等。在Java中异常被当作是对象处理,所有的异常都基于一个超类:Throwable。本文主要讲解Java语言中的异常处理体系。

Java异常处理体系

Java中所有的异常都始于一个基类Throwable。我们就从Throwable讲起。

Throwable中的常用方法

Throwable中定义了一些公共的方法,所有的异常类都可以使用。其中常用的方法:

  • getCause():返回抛出异常的原因。如果 cause 不存在或未知,则返回 null。
  • getMessage():返回异常的消息信息。
  • printStackTrace():对象的堆栈跟踪输出至错误输出流,作为字段System.err的值。

这些方法用于发生异常时获取异常和堆栈信息,帮助我们快速的定位排查问题。

Java异常体系结构

Java中的所有异常(各种ExceptionError)都是java.lang.Throwable超类的子类。这些异常分为两大类:ErrorException

Error是程序无法处理的错误,表示程序运行期间发生较严重的问题。比如OutOfMemoryErrorThreadDeath等。这些异常发生时, Java虚拟机(JVM)一般会选择线程终止。

Exception是程序本身可以处理的异常,这种异常分两大类运行时异常非运行时异常。 程序中应当尽可能去处理这些异常。

运行时异常

运行时异常都是RuntimeException类及其子类异常,如 NullPointerExceptionIndexOutOfBoundsException 等,这些异常是非检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。

非运行时异常

非运行时异常RuntimeException以外的异常,类型上都属于Exception类及其子类。 从程序语法角度讲是必须进行处理的异常,属于检查异常。如果不处理,程序就不能编译通过。 如IOExceptionSQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

Java整个异常体系结构图

如下,笔者整理了Java(基于jdk1.8)的整个异常体系结构,谨供读者参考。

Java异常体系

Java异常体系

Java异常处理机制

在Java程序中,异常处理有两种机制:抛出异常捕获异常检查异常必须在程序中处理;非检查异常,程序中可以选择捕获处理,也可以不处理。

抛出异常

Java中我们通过throwthrows关键字来抛出异常。

  • throw关键字用于方法体内部,抛出一个Throwable类型的异常。如果抛出了检查异常, 则还应该在方法头部声明方法可能抛出的异常类型。该方法的调用者也必须检查处理抛出的异常。 如果所有方法都层层上抛获取的异常,最终JVM会进行处理,处理也很简单,就是打印异常消息和堆栈信息。 如果抛出的是ErrorRuntimeException,则该方法的调用者可选择处理该异常。

  • throws关键字用于方法体外部的方法声明部分,用来声明方法可能会抛出某些异常。仅当抛出了检查异常, 该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出, 而不是囫囵吞枣一般在catch块中打印一下堆栈信息做个勉强处理。

捕获异常

捕获异常用trycatchfinally关键字。

  1. trycatchfinally三个语句块均不能单独使用,三者可以组成 try...catch...finallytry...catchtry...finally三种结构,catch语句可以有一个或多个,finally语句最多一个。

  2. trycatchfinally三个代码块中变量的作用域为代码块内部,分别独立而不能相互访问。 如果要在三个块中都可以访问,则需要将变量定义到这些块的外面。

  3. 多个catch块时候,其参数类型必须按照从子类到父类顺序由上到下,否则会导致捕获的异常不够精确。因为catch一旦匹配到一个类型,就会忽略往后的catch。比如IOException必须放到Exception前面,否则编译器会报错。

小技巧

因为finally代码块每次都要执行,我们通常会在finally中写一些清理资源的代码,但有时候这样写会有一些问题。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private BufferedReader in;
public void inputFile(String fileName) throws Exception {
try {
in = new BufferedReader(new FileReader(fileName));
} catch (FileNotFoundException e) {
//文件没找到异常
throw e;
} catch (Exception e) {
try {
//如果成功打开文件,此时才需要关闭文件
in.close();
} catch (IOException ioException) {
}
throw e;
} finally {
//不要在这里关闭文件,因为有可能这个文件根本就没有打开
}
}

最安全的方法是实用嵌套的try子句。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private BufferedReader in;
public void inputFile(String fileName) throws Exception {
try {
in = new BufferedReader(new FileReader(fileName));
try {
String s;
while ((s = in.readLine()) != null) {
//do something
}
} catch (Exception e) {
} finally {
//只有成功打开的文件才需要关闭
in.close();
}
} catch (FileNotFoundException e) {
//此时只要处理找不到文件异常即可
throw e;
}
}

引申

  • 若一段代码前有异常抛出,并且这个异常没有被捕获,这段代码将产生编译时错误「无法访问的语句」。

  • 若一段代码前有异常抛出,并且这个异常被try...catch所捕获,若此时catch语句中没有抛出新的异常,则这段代码能够被执行,否则,同第上一条。

  • 若在一个条件语句中抛出异常,则程序能被编译,但后面的语句不会被执行。

关于finally

  • finally中的代码总是会被执行,除非在执行try或者catch语句时虚拟机退出(System.exit(1))。
  • finally块可以做一些资源清理工作,如关闭文件、关闭游标等操作。
  • finally块不是必须的。
  • 如果在tryfinally块中都执行了return语句,最终返回的将是finally中的return值。

异常处理的一般原则

  1. 在知道该如何处理的情况下尽早捕获异常。否则继续向上抛出或者转译为RuntimeException——避免抛出过多的异常,影响程序可读性。
  2. 自定义非检查型异常(RuntimeException),用以封装所有的检查型异常——让程序决定是否对异常进行处理,防止吞食则有害问题的发生。
  3. 只针对异常的情况才使用异常。不要滥用try-catch,因为会影响性能。
  4. 为应用系统定义一套属于自己的异常处理框架。这样当异常发生时,才能将异常信息以统一的风格、优雅的反馈给用户。

异常的转译

如果方法抛出的异常与它执行的任务没有明显的联系,这种情形会使人不知所措。为了避免这个问题,更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常,这种做法被称为异常转译(exception translation),如下:

1
2
3
4
5
try{
// use lower-level abstraction to do our bidding
} catch(LowerLevelException ex){
throw new HigherLevelException(...);
}

异常转译的哲学

异常转译是针对所有继承Throwable超类的类而言的,从语法角度讲,其子类之间都可以相互转换。 但是从设计的角度出发,对于异常转译,我们要有一个合理的转译规则,否则各种异常互相转译必然导致代码混乱不堪。笔者认为,一个合理的三大类异常(Error, RuntimeException, 非RuntimeException)的转译关系如下图:

异常转译

异常转译

Error检查型异常非RuntimeException)转译成RuntimeException是为了增加代码的可读性、挽回因错误(Error)发生带来的负面影响,使代码更为简洁,且有利于异常的统一处理。

异常链

异常的转译涉及到了异常链的概念。在捕获一个异常后,抛出另一个异常,把底层的异常信息传给上层,并且保留底层的异常信息,这就是异常链。JDK1.4以后,Throwable子类在构造器中可以接受一个cause对象(Throwable对象)作为参数,表示原始异常,通过cause参数把原始异常传递给新的上层异常。这样,位于高层的异常递归调用getCause()方法,就可以遍历各层的异常信息。

设计自己的异常处理框架

  • 对于一个应用系统来说,发生的所有异常在用户看来都是应用系统内部的异常。因此,系统内部的异常应该统一的转译成AppAbstractException(或是其他你喜欢的名字),AppAbstractException异常应该能够提供给客户端友好的异常信息。
  • 自定义的异常都应该是RuntimeException非检查异常)类的子类,以便由开发人员在合适的位置统一处理异常。
应用异常处理框架

应用异常处理框架

eg:Spring中的所有异常都可以用 org.springframework.core.NestedRuntimeException 来表示,并且该基类继承的是RuntimeException

坚持原创技术分享,您的支持将鼓励我继续创作!