Java 教程是为 JDK 8 编写的。本页中描述的示例和实践未利用在后续版本中引入的改进。
现在你已经知道了什么是异常以及如何使用它们,现在是时候了解在程序中使用异常的优势了。
异常提供了一种方法,可以在程序的主要逻辑中分离出异常情况时要执行的操作的详细信息。在传统的编程中,错误检测,报告和处理通常会导致混乱的意大利面条代码。例如,考虑这里的伪代码方法将整个文件读入内存。
readFile {
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
}
乍一看,这个功能似乎很简单,但它忽略了以下所有潜在的错误。
要处理此类情况,readFile 函数必须具有更多代码才能执行错误检测,报告和处理。以下是函数可能的示例。
errorCodeType readFile {
initialize errorCode = 0;
open the file;
if (theFileIsOpen) {
determine the length of the file;
if (gotTheFileLength) {
allocate that much memory;
if (gotEnoughMemory) {
read the file into memory;
if (readFailed) {
errorCode = -1;
}
} else {
errorCode = -2;
}
} else {
errorCode = -3;
}
close the file;
if (theFileDidntClose && errorCode == 0) {
errorCode = -4;
} else {
errorCode = errorCode and -4;
}
} else {
errorCode = -5;
}
return errorCode;
}
这里有很多错误检测,报告和返回,原始的七行代码在杂乱中丢失了。更糟糕的是,代码的逻辑流程也已丢失,因此很难判断代码是否正在做正确的事情:如果函数无法分配足够的内存,文件是否真的被关闭了?在编写方法三个月后修改方法时,确保代码继续做正确的事情变得更加困难。许多程序员只是忽略它来解决这个问题 程序崩溃时报告错误。
异常使你可以编写代码的主要流并在其他地方处理异常情况。如果 readFile 函数使用异常而不是传统的错误管理技术,则它看起来更像是以下内容。
readFile {
try {
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
} catch (fileOpenFailed) {
doSomething;
} catch (sizeDeterminationFailed) {
doSomething;
} catch (memoryAllocationFailed) {
doSomething;
} catch (readFailed) {
doSomething;
} catch (fileCloseFailed) {
doSomething;
}
}
请注意,异常不会使你无需执行检测,报告和处理错误的工作,但它们确实可以帮助你更有效地组织工作。
异常的第二个优点是能够在方法的调用堆栈中传播错误报告。假设 readFile 方法是主程序进行的一系列嵌套方法调用中的第四种方法:method1 调用 method2,它调用 method3,最后调用 readFile。
method1 {
call method2;
}
method2 {
call method3;
}
method3 {
call readFile;
}
假设 method1 是唯一对 readFile 中可能发生的错误感兴趣的方法。传统的错误通知技术强制 method2 和 method3 将 readFile 返回的错误代码传播到调用堆栈,直到错误代码最终达到 method1 - 唯一对它们感兴趣的方法。
method1 {
errorCodeType error;
error = call method2;
if (error)
doErrorProcessing;
else
proceed;
}
errorCodeType method2 {
errorCodeType error;
error = call method3;
if (error)
return error;
else
proceed;
}
errorCodeType method3 {
errorCodeType error;
error = call readFile;
if (error)
return error;
else
proceed;
}
回想一下,Java 运行时环境通过调用堆栈向后搜索,以找到对处理特定异常感兴趣的任何方法。一个方法可以避开它内部抛出的任何异常,从而允许调用堆栈上更远的一个方法来捕获它。因此,只有关心错误的方法才需要担心检测错误。
method1 {
try {
call method2;
} catch (exception e) {
doErrorProcessing;
}
}
method2 throws exception {
call method3;
}
method3 throws exception {
call readFile;
}
但是,正如伪代码所示,避免异常需要中间人方法的一些努力。可以在方法中抛出的任何检查型异常必须在其 throws 子句中指定。
因为在程序中抛出的所有异常都是对象,所以异常的分组或分类是类层次结构的自然结果。Java 平台中一组相关异常类的示例是 java.io 中定义的那些类 IOException 及其后代。IOException 是最常用的,表示执行 I/O 时可能发生的任何类型的错误。它的后代表示更具体的错误。例如,FileNotFoundException 表示无法在磁盘上找到文件。
方法可以编写可以处理非常特定异常的特定处理程序。FileNotFoundException 类没有后代,因此以下处理程序只能处理一种类型的异常。
catch (FileNotFoundException e) {
...
}
方法可以通过在 catch 语句中指定任何异常的超类来基于其组或常规类型捕获异常。例如,要捕获所有 I/O 异常,无论其具体类型如何,异常处理程序可指定一个 IOException 参数。
catch (IOException e) {
...
}
此处理程序将能够捕获所有 I/O 异常,包括 FileNotFoundException,EOFException 等。你可以通过查询传递给异常处理程序的参数来查找有关所发生情况的详细信息。例如,使用以下命令打印堆栈跟踪。
catch (IOException e) {
// Output goes to System.err.
e.printStackTrace();
// Send trace to stdout.
e.printStackTrace(System.out);
}
你甚至可以在这里设置一个异常处理程序来处理任何 Exception。
// A (too) general exception handler
catch (Exception e) {
...
}
Exception 类靠近 Throwable 类层次结构的顶部。因此,除了处理程序要捕获的异常之外,此处理程序还将捕获许多其他异常。你可能希望以这种方式处理异常,例如如果你希望程序执行的所有操作就是为用户打印出错误消息然后退出。
但是,在大多数情况下,你希望异常处理程序尽可能具体。原因是处理程序必须做的第一件事,是在确定最佳恢复策略之前确定发生了什么类型的异常。实际上,通过不捕获特定错误,处理程序必须适应任何可能性。过于笼统的异常处理程序会使代码更容易出错,因为其捕获和处理的异常是程序员未预料到的并且处理程序不意图处理的。
如上所述,你可以创建异常组并以一般方式处理异常,或者你可以使用特定的异常类型来区分异常并以精确的方式处理异常。