前言

本章摘录自 Java 核心技术 卷1 基础知识,主要是异常的一些基础知识,以及一些使用的建议,重点部分已用红色字体标出。

正文

异常分类

异常 - 图1

所有的异常都是由 Throwable 继承而来,下一层分为 Error 和 Exception。

  • Error:Java 运行时系统的内部错误和资源耗尽错误。 例如,OutOfMemoryError 系统运行时内存不够,StackOverflowError 系统运行时堆栈溢出等严重情况。应用程序无法处理该类异常。

  • Exception:其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。例如,NullPointerException 空指针异常,FileNotFoundException 试图读取不存在的文件等异常。

Exception 下层又分为两个分支:一个分支派生于 RuntimeException;另一个分支包含其他异常。

  • 派生于 RuntimeException 的异常:运行时异常。是指编译器不要求强制处置的异常。一般是指编程时的逻辑错误,是程序员应该积极避免其出现的异常。

  • 不是派生于 RuntimeException 的异常:编译时异常。是指编译器要求必须处置的异常。即程序在运行时由于外界因素造成的一般性异常。

划分两个分支的规则是:由程序错误导致的异常属于 RuntimeException;而程序本身没有问题,但由于像 I/O 错误这类问题导致的异常属于其他异常。

Java 语言规范将派生于 Error 类或 RuntimeException 类的所有异常称为非受检异常,所有其他的异常称为受检异常,上图中也进行了说明。

声明受检异常

在如下两种情况时,需要声明受检异常:

  1. 调用一个抛出受査异常的方法,例如,FileInputStream 构造器。
  2. 程序运行过程中发现错误,并且利用 throw 语句抛出一个受查异常。

在方法的首部声明这个方法可能抛出的异常,如下所示:

  1. public class CheckedExceptionDemo {
  2. public Image loadImage(String s) throws IOException {
  3. // 调用FileInputStream构造器加载文件流
  4. }
  5. }

如果一个方法有可能抛出多个受检异常类型,那么就必须在方法的首部列出所有的异常类。每个异常类之间用逗号隔开。

  1. public class CheckedExceptionDemo {
  2. public Image loadImage(String s) throws FileNotFoundException, IOException {
  3. // 调用FileInputStream构造器加载文件流
  4. // 程序运行过程中,利用throw语句抛出IOException
  5. if (...) {
  6. throw new IOException("Invalid file path");
  7. }
  8. }
  9. }

不需要声明非受检异常,比如派生于 Error 或者 RuntimeException 的异常。

  1. public class CheckedExceptionDemo {
  2. public void drawImage(int i) throws ArrayIndexOutOfBoundsException { // 不需要声明
  3. }
  4. }

另外,关于声明受检异常,我们还需要注意一点。如果当前类存在子类,且子类覆盖了父类的方法,子类方法中声明的受检异常不能比超类方法中声明的异常更通用。比如父类的方法声明了 IOException 异常,那子类的方法只能声明 IOException 异常或者 IOException 异常的子类(FileNotFoundException)或者不抛出异常,不能声明 Exception 异常。如果父类方法没有抛出任何受检异常,则子类也不能抛出任何受检异常。

自定义异常类

自定义一个异常类,比较简单,只需要定义一个派生于 Exception 的类,或者派生于 Exception 的子类的类。

一般我们需要考虑的是,定义一个派生于 RuntimeException 的异常类,还是定义一个派生于 Exception 的异常类?

这里再强调一下划分两者的规则是:由程序错误导致的异常属于 RuntimeException;而程序本身没有问题,但由于像 I/O 错误这类问题导致的异常属于其他异常。

一般,业务中定义的异常类都派生于 RuntimeException。

  1. public class ServiceException extends RuntimeException {
  2. private static final long serialVersionUID = -7541127765936561867L;
  3. public ServiceException() {
  4. super();
  5. }
  6. public ServiceException(String message) {
  7. super(message);
  8. }
  9. public ServiceException(Throwable cause) {
  10. super(cause);
  11. }
  12. public ServiceException(String message, Throwable cause) {
  13. super(message, cause);
  14. }
  15. }

捕获异常

设置try/catch语句块,捕获代码中的异常。介绍几种try/catch语句块的形式:

  1. 最简单的try/catch语句块。

    1. try {
    2. // code
    3. } catch (ExceptionType e) {
    4. // handler for this type
    5. }
    1. 如果在 try 语句块中的任何代码抛出了一个在 catch 子句中说明的异常类,那么程序会跳过 try 语句块的其余代码,执行 catch 子句中的处理器代码。
    2. 如果在 try 语句块中的代码没有拋出任何异常,那么程序将跳过 catch 子句。
    3. 如果方法中的任何代码拋出了一个在 catch 子句中没有声明的异常类型,那么这个方法就会立刻退出。
  2. 捕获多个异常的try/catch语句块。

    1. try {
    2. // code that might throw exceptions
    3. } catch (FileNotFoundException e) {
    4. // emergency action for missing files
    5. } catch (UnknownHostException e) {
    6. // emergency action for unknown hosts
    7. } catch (IOException e) {
    8. // emergency action for all other I/O problems
    9. }
  3. 同一个 catch 子句捕获多个异常(Java SE 7+)。

    1. try {
    2. // code that might throw exceptions
    3. } catch (FileNotFoundException | UnknownHostException e) {
    4. // emergency action for missing files and unknown hosts
    5. } catch (IOException e) {
    6. // emergency action for all other I/O problems
    7. }

    捕获多个异常时,异常变量(e)隐含为 final 变量,不能在 catch 子句体中为 e 赋不同的值。

  4. finally 子句,用来关闭申请的资源。

    1. InputStream in = new FileInputStream(...);
    2. try {
    3. // 1
    4. // code that might throw exceptions
    5. // 2
    6. } catch (IOException e) {
    7. // 3
    8. // show error message
    9. // 4
    10. } finally {
    11. // 5
    12. in.close();
    13. }
    14. // 6
    1. 代码没有抛出异常。在这种情况下,程序首先执行 try 语句块中的全部代码,然后执行 finally 子句中的代码。随后,继续执行 try 语句块之后的代码。执行顺序 1、2、5、6。
    2. try 语句块抛出一个 IOException 异常。在这种情况下,程序跳过 try 语句块中的剩余代码,转去执行 catch 子句中的代码,然后执行 finally 子句中的代码。如果 catch 子句没有抛出异常,程序将继续执行 try 语句块之后的代码。执行顺序 1、3、4、5、6。如果 catch 子句也抛出了一个异常,异常将被抛回这个方法的调用者。执行顺序 1、3、5。
    3. try 语句块抛出了一个异常,但这个异常不是由 catch 子句捕获的。在这种情况下,程序跳过 try 语句块中的剩余代码,然后执行 finally 子句中的代码,并将异常抛给这个方法的调用者。执行顺序 1、5。
  5. try 语句可以只有 finally 子句,而没有 catch 子句。

    1. InputStream in = ...;
    2. try {
    3. // code that might throw exceptions
    4. } finally {
    5. in.close();
    6. }

    无论在 try 语句块中是否遇到异常,finally 子句中的 in.close() 语句都会被执行。当然,如果 try 语句块抛出了一个异常,因为这里并没有使用 catch 子句处理,所以这个异常会继续往上抛出。

  6. 解搞合 try/catch 和 try/finally 语句块。

    1. InputStream in = ...;
    2. try {
    3. try {
    4. // code that might throw exceptions
    5. } finally {
    6. in.close();
    7. }
    8. } catch (IOException e) {
    9. // show error message
    10. }

    在 Java 核心技术 卷1 中提供了以上有意思的写法。内层的 try 语句块只有一个职责,就是确保关闭输入流。外层的 try 语句块也只有一个职责,就是捕获可能出现的异常。 这种设计方式不仅清楚,而且还有一个功能,就是可以同时捕获 finally 子句中出现的异常。

  7. finally 子句中不能包含 return 语句。

当 finally 子句包含 return 语句时,将会出现一种意想不到的结果,假设利用 return 语句从 try 语句块中退出。在方法返回前,finally 子句的内容将被执行。如果 finally 子句中也有一个 return 语句,这个返回值将会覆盖原始的返回值。

  1. public static int f(int n) {
  2. try {
  3. int r = n * n;
  4. return r;
  5. } finally {
  6. if (n == 2) return 0;
  7. }
  8. }

如果调用 f(2),那么 try 语句块的计算结果为 r = 4,并执行 return 语句然而,在方法真正返回前,还要执行 finally 子句。finally 子句将使得方法返回 0,这个返回值覆盖了原始的返回值 4。

  1. 带资源的 try 语句(Java SE 7+)。

假设在 try 语句块中的代码会抛出一个异常,执行 finally 语句块,close 方法也抛出一个异常,则 try 语句块中的异常会被丢弃,转而抛出 close 方法的异常。如果需要处理这种情况,只抛出 try 语句块中的异常,代码会变得比较繁琐。

  1. InputStream in = ...;
  2. Exception ex = null;
  3. try {
  4. // code that might throw exceptions
  5. } catch (Exception e) {
  6. ex = e;
  7. throw e;
  8. } finally {
  9. try {
  10. if (in != null) {
  11. in.close();
  12. }
  13. } catch (Exception e) {
  14. if (ex == null) {
  15. throw e;
  16. }
  17. }
  18. }

Java SE 7 中推出了新的特性,支持带资源的 try 语句(try-with-resources),来代替 finally 子句关闭资源。假设当前资源属于一个实现了 AutoCloseable 接口的类,使用以下带资源的 try 语句可以实现自动关闭资源。

  1. try (Resource res = ...) {
  2. work with res
  3. }

try 块退出时,会自动调用 res.close() 方法。

带资源的 try 语句支持指定一个或多个资源:

  1. try (Scanner in = new Scanner(new FileInputStream("7usr/share/dict/words"), "UTF-8")) {
  2. // code
  3. } catch (FileNotFoundException e) {
  4. // handler for this type
  5. }
  6. try (Scanner in = new Scanner(new FileInputStream("7usr/share/dict/words"), "UTF-8");
  7. PrintWriter out = new PrintWriter("out.txt")) {
  8. // code
  9. } catch (FileNotFoundException e) {
  10. // handler for this type
  11. }

这个块正常退出时,或者存在异常时,in 和 out 都会关闭,就好像使用了 finally 块一样。

如果 try 块抛出一个异常,而且 close 方法也抛出一个异常。带资源的 try 语句可以很好地处理这种情况。原来的异常会重新抛出,而 close 方法抛出的异常会“被抑制”。这些异常将自动捕获,并由 addSuppressed 方法增加到原来的异常。如果对这些异常感兴趣,可以调用 getSuppressed 方法,它会得到从 close 方法抛出并被抑制的异常列表。

所以,涉及到需要关闭资源的操作,建议都使用带资源的 try 语句(try-with-resources)。

再次抛出异常与异常链

在 catch 子句中再次抛出异常,建议将原始异常设置为新异常的“原因”。

  1. public class ExceptionChainDemo {
  2. public void fun1() throws FileNotFoundException {
  3. throw new FileNotFoundException("fun1");
  4. }
  5. public void fun2() throws IOException {
  6. try {
  7. fun1();
  8. } catch (FileNotFoundException e) {
  9. throw new IOException("fun2", e);
  10. }
  11. }
  12. public static void main(String[] args) throws Exception {
  13. ExceptionChainDemo test2 = new ExceptionChainDemo();
  14. test2.fun2();
  15. }
  16. }

运行代码,输出:

  1. Exception in thread "main" java.io.IOException: fun2
  2. at test7.ExceptionChainDemo.fun2(ExceptionChainDemo.java:22)
  3. at test7.ExceptionChainDemo.main(ExceptionChainDemo.java:28)
  4. Caused by: java.io.FileNotFoundException: fun1
  5. at test7.ExceptionChainDemo.fun1(ExceptionChainDemo.java:15)
  6. at test7.ExceptionChainDemo.fun2(ExceptionChainDemo.java:20)
  7. ... 1 more

因为在 catch 子句中再次抛出异常使用了如下构造器,将 fun1 方法中抛出的异常设置为新异常的“原因”,所以控制台在打印异常栈的时候,可以追踪到整个异常链。

  1. public IOException(String message, Throwable cause) {
  2. super(message, cause);
  3. }

如果 IOException 中没有提供这样的构造器,可以使用 Throwable 的 initCause 方法,初始化新异常的“原因”。

  1. public void fun2() throws IOException {
  2. try {
  3. fun1();
  4. } catch (FileNotFoundException e) {
  5. IOException e2 = new IOException("fun2");
  6. e2.initCause(e);
  7. throw e2;
  8. }
  9. }

执行结果和上面的代码一样。

如果 catch 子句中没有初始化新异常的“原因”,则不会打印 fun1 方法中抛出的异常。

  1. public void fun2() throws IOException {
  2. try {
  3. fun1();
  4. } catch (FileNotFoundException e) {
  5. throw new IOException("fun2");
  6. }
  7. }

运行代码,输出:

  1. Exception in thread "main" java.io.IOException: fun2
  2. at test7.ExceptionChainDemo.fun2(ExceptionChainDemo.java:22)
  3. at test7.ExceptionChainDemo.main(ExceptionChainDemo.java:28)

转载

  • Java 核心技术 卷1 基础知识 第10版

作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/qsqxdg 来源:殷建卫 - Java 开发笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。