这些Java教程是针对JDK 8编写的。本页面中描述的示例和实践不利用后续版本中引入的改进,并可能使用不再可用的技术。
请参阅Java语言更改以获取Java SE 9及后续版本中更新的语言特性摘要。
请参阅JDK版本说明以了解所有JDK版本中的新功能、增强功能以及已删除或已弃用选项的信息。
现在你已经了解了什么是异常以及如何使用它们,现在是时候学习在程序中使用异常的优势了。
异常提供了一种方法,可以将在发生异常情况时要做的事情的细节与程序的主逻辑分离开来。在传统编程中,错误检测、报告和处理常常导致混乱的代码。例如,考虑下面这个将整个文件读入内存的伪代码方法。
readFile { 打开文件; 确定文件大小; 分配相应的内存; 将文件读入内存; 关闭文件; }
乍一看,这个函数似乎足够简单,但它忽略了以下潜在的错误。
为了处理这些情况,readFile
函数必须有更多的代码来进行错误检测、报告和处理。以下是函数可能看起来像的示例。
errorCodeType readFile { 初始化 errorCode = 0; 打开文件; if (文件已打开) { 确定文件的长度; if (得到文件长度) { 分配相应的内存; if (得到足够的内存) { 将文件读入内存; if (读取失败) { errorCode = -1; } } else { errorCode = -2; } } else { errorCode = -3; } 关闭文件; if (文件未关闭 && errorCode == 0) { errorCode = -4; } else { errorCode = errorCode and -4; } } else { errorCode = -5; } return errorCode; }
这里有很多错误检测、报告和返回,原本的七行代码在混乱中丢失了。更糟糕的是,代码的逻辑流也丢失了,因此很难确定代码是否做正确的事情:如果函数无法分配足够的内存,文件真的会关闭吗?当你在编写三个月后修改该方法时,确保代码继续做正确的事情甚至更困难。许多程序员通过简单地忽略它来解决这个问题 - 当程序崩溃时报告错误。
异常使你能够编写代码的主流程,并将异常情况处理在其他地方。如果 readFile
函数使用异常而不是传统的错误管理技术,它将更像以下代码。
readFile { try { 打开文件; 确定文件大小; 分配相应大小的内存; 将文件读入内存; 关闭文件; } catch (文件打开失败) { 处理错误操作; } catch (确定文件大小失败) { 处理错误操作; } catch (内存分配失败) { 处理错误操作; } catch (文件读取失败) { 处理错误操作; } catch (文件关闭失败) { 处理错误操作; } }
需要注意的是,异常并不会减轻检测、报告和处理错误的工作,但它们能更有效地帮助你组织这些工作。
异常的第二个优势是能够将错误报告传播至方法调用堆栈。假设readFile
方法是主程序中一系列嵌套方法调用的第四个方法:method1
调用method2
,method2
调用method3
,最终method3
调用readFile
。
method1 { 调用method2; } method2 { 调用method3; } method3 { 调用readFile; }
假设只有method1
关心readFile
中可能发生的错误。传统的错误通知技术强制method2
和method3
将readFile
返回的错误代码传播至调用堆栈,直到最终达到只关心这些错误的method1
。
method1 { 错误码类型 error; error = 调用method2; if (error) 处理错误; else 继续执行; } 错误码类型 method2 { 错误码类型 error; error = 调用method3; if (error) return error; else 继续执行; } 错误码类型 method3 { 错误码类型 error; error = 调用readFile; if (error) return error; else 继续执行; }
回想一下,Java运行时环境会向后搜索调用堆栈,找到对特定异常感兴趣的方法。方法可以规避其内部抛出的任何异常,从而允许调用堆栈中更高层的方法捕获它。因此,只有关心错误的方法需要担心检测错误。
method1 { try { 调用method2; } catch (异常 e) { 处理错误; } } method2 throws 异常 { 调用method3; } method3 throws 异常 { 调用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) { // 输出发送到System.err。 e.printStackTrace(); // 将跟踪发送到stdout。 e.printStackTrace(System.out); }
您甚至可以设置一个处理任何Exception
的异常处理程序。
// 一个(太)通用的异常处理程序 catch (Exception e) { ... }
Exception
类接近Throwable
类层次结构的顶部。因此,此处理程序将捕获除了处理程序预期捕获的异常之外的许多其他异常。如果您的程序只是要打印出一个错误消息给用户,然后退出,您可能希望以这种方式处理异常。
然而,在大多数情况下,您希望异常处理程序尽可能具体。原因是处理程序必须首先确定发生了什么类型的异常,然后才能决定最佳恢复策略。实际上,通过不捕获特定的错误,处理程序必须适应任何可能性。处理程序过于通用可能会使代码更容易出错,因为它捕获和处理程序员未预料到的异常,而处理程序并不适用于这些异常。
正如注意到的那样,您可以创建异常组并以一般方式处理异常,或者可以使用特定的异常类型来区分异常并以精确方式处理异常。