5.7 使用异常带来的优势
现在已经知道了什么是异常,以及如何使用它们,接下来将会介绍在程序中使用异常的优点。
5.7.1 将错误处理代码与“常规”代码分离
在传统的编程中,错误检测、报告和处理常常导致混淆意大利面条代码(spaghetti code)。而异常提供了一种方式,可以区分开当主逻辑发生异常情况时不同的处理细节。例如,以下示例会将整个文件读入内存。
readFile { open the file; determine its size; allocate that much memory; read the file into memory; close the file; }
乍一看,这个功能很简单,但它忽略了以下所有潜在错误。
· 无法打开文件会发生什么?
· 无法确定文件的长度会发生什么?
· 不能分配足够的内存会发生什么?
· 读取失败会发生什么?
· 文件无法关闭会怎么样?
为了处理这些情况,readFile函数必须有更多的代码来执行错误检测、报告和处理。下面展示该函数可能会是什么样子。
这里面有很多错误检测、报告的细节,使得原来的7行代码被淹没在这些代码中。更糟的是,代码的逻辑流已经丢失,因此很难判断代码是否正确。在编写该方法三个月后再来修改这个方法时,难以确保代码能够继续正确运行。因此,许多程序员会通过简单地忽略它来解决这个问题。这样当他们的程序崩溃时,就会生成报告错误。
异常是开发人员编写代码的主要流程,并处理其他地方的特殊情况。如果readFile函数使用异常而不是传统的错误管理技术,应该更像下面的示例:
注意,异常不会减少你在法执行检测、报告和处理错误方面的工作,但它们可以帮助你更有效地组织工作。
5.7.2 将错误沿调用堆栈向上传递
异常的第二个优点是能够在方法的调用堆栈上将错误向上传递。假设readFile方法是在主程序进行的一系列嵌套方法中的最底层,比如首先是method1调用method2,而后调用method3,最后调用readFile。
假设method1是对readFile中可能发生的错误感兴趣的唯一方法。传统的错误通知技术强制method2和method3将readFile返回的错误代码传递到调用堆栈,直到错误代码最终到达method1。
回想一下,Java运行时环境通过调用堆栈向后搜索以找到任何对处理特定异常感兴趣的方法。一个方法可以阻止在其中抛出的任何异常,从而允许另外一个方法在调用栈上更远的地方来捕获它。因此,只有关心错误的方法才需要担心检测错误。
如上述伪代码所示,任何可以在方法中抛出的已检查异常都必须在throws子句中指定。
5.7.3 对错误类型进行分组和区分
在程序中抛出的所有异常都是对象,异常的分组或分类是类层次结构的自然结果。Java平台中一组相关异常类的示例是IOException。IOException是最常见的,表示执行I/O时可能发生的任何类型的错误。它的后代表示更具体的错误。例如,FileNotFoundException意味着文件无法在磁盘上找到。
一个方法可以处理非常特定的异常。FileNotFoundException类没有后代,因此下面的处理程序只能处理一种类型的异常。
可以在catch语句中指定任何异常的超类来基于其组或常规类型捕获异常。例如,为了捕获所有I/O异常,无论其具体类型如何,异常处理程序都会指定一个IOException参数。
这个处理程序将能够捕获所有I/O异常,包括FileNotFoundException、EOFException等。你可以通过查询传递给异常处理程序的参数来查找有关发生的详细信息。例如,使用以下命令打印堆栈跟踪。
下面的例子可以处理所有的异常:
Exception类接近Throwable类层次结构的顶部。因此,这个处理程序将会捕获除处理程序想要捕获的那些异常之外的许多其他异常。
在大多数情况下,异常处理程序应该尽可能的具体。原因是处理程序必须做的第一件事是在选择最佳恢复策略之前要确定发生的是什么类型的异常。实际上,如果不捕获特定的错误,处理程序必须适应任何可能性。太过通用的异常处理程序可能会捕获和处理程序员不期望并且处理程序不想要的异常,从而使代码更容易出错。
如上所述,开发人员可以以常规方式创建异常分组来处理异常,也可以使用特定的异常类型来区分异常,从而可以以确切的方式来处理异常。