假设在一个 Java 程序运行期间出现了一个错误。这个错误可能是由于文件包含了错误信息,或者网络连接出现问题造成的,也有可能是因为使用无效的数组下标,或者试图使用一个没有被赋值的对象引用而造成的。用户期望在出现错误时,程序能够采用一些理智的行为。如果由于出现错误而使得某些操作没有完成,程序应该:

  • 返回到一种安全状态,并能够让用户执行一些其他的命令;
  • 允许用户保存所有操作的结果,并以妥善的方式终止程序。

对于方法中的一个错误,传统的做法是返回一个特殊的错误码,由调用方法分析。
遗憾的是,并不是在任何情况下都能够返回一个错误码。有可能无法明确地将有效数据与无效数据加以区分。一个返回整型的方法就不能简单地通过返回 –1 表示错误,因为 –1 很可能是一个完全合法的结果。
在 Java 中,如果某个方法不能够采用正常的途径完整它的任务,就可以通过另外一个路径退出方法。在这种情况下,方法并不返回任何值,而是抛出(throw)一个封装了错误信息的对象。这个方法将会立刻退出,并不返回任何值。此外,调用这个方法的代码也将无法继续执行,取而代之的是,异常处理机制开始搜索能够处理这种异常状况的异常处理器(exception handler)。

异常分类

在 Java 程序设计语言中,异常具有自己的语法和特定的继承结构:异常对象都是派生于 Throwable 类的一个实例。

  1. ┌───────────┐
  2. Object
  3. └───────────┘
  4. ┌───────────┐
  5. Throwable
  6. └───────────┘
  7. ┌─────────┴─────────┐
  8. ┌───────────┐ ┌───────────┐
  9. Error Exception
  10. └───────────┘ └───────────┘
  11. ┌───────┘ ┌────┴──────────┐
  12. ┌─────────────────┐ ┌─────────────────┐┌───────────┐
  13. OutOfMemoryError │... RuntimeException ││IOException│...
  14. └─────────────────┘ └─────────────────┘└───────────┘
  15. ┌───────────┴─────────────┐
  16. ┌─────────────────────┐ ┌─────────────────────────┐
  17. NullPointerException IllegalArgumentException │...
  18. └─────────────────────┘ └─────────────────────────┘

可以看到,所有的异常都是由 Throwable 继承而来,但在下一层立即分解为两个分支:Error 和 Exception。
Error 类层次结构描述了 Java 运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这种类型的对象。如果出现了这样的内部错误,除了通告给用户,并尽力使程序安全地终止。
Exception 类层次结构又分解为两个分支:一个分支派生于 RuntimeException;另一个分支包含其他异常。划分两个分支的规则是:由程序错误导致的异常属于 RuntimeException;而程序本身没有问题,但由于像 I/O 错误这类问题导致的异常属于其他异常。

可以将 RuntimeException 看成程序中的逻辑错误;非 RuntimeException 异常看成由于不可预测的原因所引发的异常的基类。

派生于 RuntimeException 的异常包含下面几种情况:

  • 错误的类型转换。
  • 数组访问越界。
  • 访问 null 指针。

不是派生于 RuntimeException 的异常包括:

  • 试图在文件尾部后面读取数据。
  • 试图打开一个不存在的文件。
  • 试图根据给定的字符串查找 Class 对象,而这个字符串表示的类并不存在。

「如果出现 RuntimeException 异常,那么就一定是你的问题」是一条相当有道理的规则

Java 语言规范将派生于 Error 类或 RuntimeException 类的所有异常称为非受查(unchecked)异常,所有其他的异常称为受查(checked)异常。编译器将核查是否为所有的受查异常提供了异常处理器,也就是是否捕获。

编译器对 RuntimeException 及其子类不做强制捕获要求,不是指应用程序本身不应该捕获并处理 RuntimeException。是否需要捕获,具体问题具体分析

声明受查(checked)异常

如果遇到了无法处理的情况,那么Java的方法可以抛出一个异常。一个方法不仅需要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误。例如,一段读取文件的代码知道有可能读取的文件不存在,或者内容为空,因此,试图处理文件信息的代码就需要通知编译器可能会抛出 IOException 类的异常。
方法应该在其首部声明所有可能抛出的异常。这样可以从首部反映出这个方法可能抛出哪类受查异常。例如,下面是标准类库中提供的 FileInputStream 类的一个构造器的声明:

  1. public FileInputStream(String name) throws FileNotFoundException

这个声明表示这个构造器将根据给定的 String 参数产生一个 FileInputStream 对象,但也有可能抛出一个 FileNotFoundException 异常。如果发生了这种糟糕情况,构造器将不会初始化一个新的 FileInputStream 对象,而是抛出一个 FileNotFoundException 类对象。
以下 4 中情况,应该抛出异常:

  • 调用一个抛出受查异常的方法,例如,FileInputStream 构造器。
  • 程序运行过程中发现错误,并且利用throw语句抛出一个受查异常
  • 程序出现错误,例如,a[–1] = 0 会抛出一个 ArrayIndexOutOfBoundsException 这样的非受查异常。
  • Java 虚拟机和运行时库出现的内部错误。

对于那些可能被他人使用的 Java 方法,应该根据异常规范(exceptionspecification),在方法的首部声明这个方法可能抛出的异常:

  1. class MyAnimation {
  2. public Image loadImage(String s) throws IOException {
  3. ...
  4. }
  5. ...
  6. }

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

  1. class MyAnimation {
  2. public Image loadImage(String s) throws FileNotFoundException, EOFException {
  3. ...
  4. }
  5. ...
  6. }

不需要声明 Java 的内部错误,即从 Error 继承的错误。任何程序代码都具有抛出那些异常的潜能,而我们对其没有任何控制能力。同样,也不应该声明从 RuntimeException 继承的那些非受查异常。
总之,一个方法必须声明所有可能抛出的受查异常,而非受查异常要么不可控制(Error),要么就应该避免发生(RuntimeException)。

如果在子类中覆盖了超类的一个方法,子类方法中声明的受查异常不能比超类方法中声明的异常更通用。如果超类方法没有抛出任何受查异常,子类也不能抛出任何受查异常。例如,如果覆盖 JComponent.paintComponent 方法,由于超类中这个方法没有抛出任何异常,所以,自定义的 paintComponent 也不能抛出任何受查异常。

如果类中的一个方法声明将会抛出一个异常,而这个异常是某个特定类的实例时,则这个方法就有可能抛出一个这个类的异常,或者这个类的任意一个子类的异常。例如,FileInputStream 构造器声明将有可能抛出一个 IOExcetion 异常,然而并不知道具体是哪种 IOException 异常。它既可能是 IOException 异常,也可能是其子类的异常,例如,FileNotFoundException。

如何抛出异常

假设,一个名为 readData 的方法正在读取一个长度为 1024 字节的文件,然而,读到733个字节之后文件就结束了。我们认为这是一种不正常的情况,希望抛出一个异常。
首先要决定应该抛出什么类型的异常。将上述异常归结为 IOException 是一种很好的选择。仔细地阅读 Java API 文档之后会发现:EOFException 异常描述的是「在输入过程中,遇到了一个未预期的 EOF 后的信号」。这正是我们要抛出的异常。下面是抛出这个异常的语句:

  1. throw new EOFException();
  2. // or
  3. EOFException e = new EOFException();
  4. throw e;

这样就可以在方法中使用:

  1. String readData(Scanner in) throws EOFException {
  2. ...
  3. while(...) {
  4. if (!in.hasNext()) { // EOF encountered
  5. if (n < len) throw new EOFException();
  6. }
  7. ...
  8. }
  9. return s;
  10. }

上述抛出异常可以用 EOFException 的另一个字符串类型参数的构造器,这个构造器可以更加细致的描述异常出现的情况:

  1. String gripe = "Content-length: " + len + ", Received: " + n;
  2. throw new EOFException(gripe);

一旦方法抛出了异常,这个方法就不可能返回到调用者。也就是说,不必为返回的默认值或错误代码担忧。

创建异常类

在程序中,可能会遇到任何标准异常类都没有能够充分地描述清楚的问题。在这种情况下,创建自己的异常类就是一件顺理成章的事情了。
例如,定义一个派生于 IOException 的类。习惯上,定义的类应该包含两个构造器,一个是默认的构造器;另一个是带有详细描述信息的构造器(超类 Throwable 的 toString 方法将会打印出这些详细信息,这在调试中非常有用):

  1. class FileFormatException extends IOException {
  2. public FileFormatException() {}
  3. public FileFormatException(String gripe) {
  4. super(gripe);
  5. }
  6. }