5.4 异常和异常处理

2.6.3 节介绍过已检异常和未检异常。本节进一步讨论异常在设计方面的问题,以及如何在你自己的代码中使用异常。

记住,在 Java 中,异常是对象。这个对象的类型是 java.lang.Throwable,更准确地说是 Throwable 类的子类,更具体地描述发生的异常是什么类型。Throwable 类有两个标准子类:java.lang.Errorjava.lang.ExceptionError 类的子类对应的异常表示不可恢复的问题,例如,虚拟机耗尽了内存,或类文件损坏了,无法读取。这种异常可以捕获并处理,但很少这么做——这种异常就是前面提到的未检异常。

Exception 类的子类对应的异常表示没那么严重的状况,可以捕获并处理,例如:java.io.EOFException,表示到达文件的末尾;java.lang.ArrayIndexOutOfBoundsException,表示程序尝试读取的元素超出了数组的末端。这种异常是第 2 章介绍过的已检异常(RuntimeException 的子类是个例外,仍然属于未检异常)。本书使用“异常”这个术语指代所有异常对象,不管是 Exception 类型还是 Error 类型。

因为异常是对象,所以可以包含数据,而且异常所属的类可以定义方法,操作这些数据。

Throwable 类及其所有子类都包含一个 String 类型的字段,存储一个人类可读的错误消息,描述发生的异常状况。这个字段的值在创建异常对象时设定,可以使用 getMessage() 方法从异常对象中读取。多数异常都只包含这个消息,但少数异常还包含其他数据。例如,java.io.InterruptedIOException 异常包含一个名为 bytesTransferred 的字段,表示在异常状况中断传输之前完成了多少输入或输出。

自己设计异常时,要考虑建模异常对象需要哪些额外信息。这些信息一般是针对中断的操作和遇到的异常状况(例如前面的 java.io.InterruptedIOException 异常)。

在应用设计中使用异常时要做些权衡。使用已检异常的话,意味着编译器能处理(或顺着调用堆栈向上冒泡)可能恢复或重试的已知状况,还意味着更难忘记处理错误,因此能减少由于忘记处理错误状况而导致系统在生产环境中崩溃的几率。

另一方面,就算理论上某些状况建模为已检异常,有些应用也无法从这些状况中恢复。例如,如果一个应用需要读取在文件系统特定位置存储的配置文件,而应用启动时找不到这个文件,尽管 java.io.FileNotFoundException 是已检异常,但除了打印错误消息并退出之外,这个应用别无他法。遇到这种情况时,假若强制处理或冒泡无法恢复的异常,近乎于背道而驰。

设计异常机制时,应该遵循下述良好的做法:

  • 考虑要在异常中存储什么额外状态——记住,异常也是对象;

  • Exception 类有四个公开的构造方法,一般情况下,自定义异常类时这四个构造方法都要实现,可用于初始化额外的状态,或者定制异常消息;

  • 不要在你的 API 中自定义很多细致的异常类——Java I/O 和反射 API 都因为这么做了而受人诟病,所以别让使用这些包时的情况变得更糟;

  • 别在一个异常类型中描述太多状况——例如,实现 JavaScript 的 Nashorn 引擎(Java 8 b的新功能)一开始有超多粗制滥造的异常,不过在发布之前修正了。

最后,还要避免使用两种处理异常的反模式:

  1. // 不要捕获异常而不处理
  2. try {
  3. someMethodThatMightThrow();
  4. } catch(Exception e){
  5. }
  6. // 不要捕获,记录日志后再重新抛出异常
  7. try {
  8. someMethodThatMightThrow();
  9. } catch(SpecificException e){
  10. log(e);
  11. throw e;
  12. }

第一个反模式直接忽略近乎一定需要处理的异常状况(甚至没有在日志中记录)。这么做会增大系统其他地方出现问题的可能性——出现问题的地方可能会离原来的位置很远。

第二个反模式只会增加干扰——虽然记录了错误消息,但没真正处理发生的问题——在系统高层的某部分代码中还是要处理这个问题。