用异常来处理错误----第二节 异常管理的优势
你已经读了有关什么是异常以及怎样使用它们的内容,现在是学习在你的程序中使用异常的好处的时候了。
优势1:把规则代码与错误处理代码分离
异常处理规定把错误发生时所要的细节工作与程序的主逻辑代码分离。在传统程序中,错误的发现、报告以及处理经常使得代码混乱。例如,思考下面的伪代码,这是一个把整个文件读入内存的方法。
1. readFile {
2. open the file;
3. determine its size;
4. allocate that much memory;
5. read the file into memory;
6. close the file;
7. }
8.
第一眼看上去,这个函数似乎很简单,但是它却忽略了所发生下面这些错误的可能。
1、 如果不能打开文件,会发生什么?
2、 如果不能判定文件的大小,会发生什么?
3、 如果没有足够的内存,会发生什么?
4、 如果读取失败,会发生什么?
5、 如果文件不能关闭。会发生什么?
要处理这些信息,readFile函数必须用更多的代码来做错误发现、报告和处理工作。这个函数看上去可能象这样:
1. errorCodeType readFile {
2. initialize errorCode = 0;
3. open the file;
4. if (theFileIsOpen) {
5. determine the length of the file;
6. if (gotTheFileLength) {
7. allocate that much memory;
8. if (gotEnoughMemory) {
9. read the file into memory;
10. if (readFailed) {
11. errorCode = -1;
12. }
13. else {
14. errorCode = -2;
15. }
16. } else {
17. errorCode = -3;
18. }
19. close the file;
20. if (theFileDidntClose && errorCode == 0) {
21. errorCode = -4;
22. } else {
23. errorCode = errorCode and -4;
24. }
25. } else {
26. errorCode = -5;
27. }
28. return errorCode;
29. }
30.
有如此多的错误发现、报告和返回,使得初的7行代码被埋没在混乱的错误代码之中。更严重的是,代码的逻辑流已经没有了,这样使得它很难说明代码是否正在做着正确的事情:如果函数在分配内存过程失败,文件真得的被关闭了吗?甚至更难保证在三个月之后,你编写的这段代码继续做正确的事情。
异常处理使你能够编写代码的主工作流并且在别的地方来处理异常信息。如果readFile函数使用异常处理来代替传统的错误管理技术,它应该像如下所示的代码这样:
1. readFile {
2. try {
3. open the file;
4. determine its size;
5. allocate that much memory;
6. read the file into memory;
7. close the file;
8. } catch (fileOpenFailed) {
9. doSomething;
10. } catch (sizeDeterminationFailed) {
11. doSomething;
12. } catch (memoryAllocationFailed) {
13. doSomething;
14. } catch (readFailed) {
15. doSomething;
16. } catch (fileCloseFailed) {
17. doSomething;
18. }
19. }
20.
注意:异常处理不会节省错误的发现、报告、处理的工作量,但是它们能够帮助你更有效的组织代码。
优势2:向调用堆栈上层传递错误
异常处理的第二个优势是向方法的调用堆栈上层传递错误报告的能力。假如readFile方法是主程序调用的一系列嵌套方法中的第四个方法:方法1调用方法2,方法2调用方法3,方法3调用readFile,代码结构如下所示:
1. method1 {
2. call method2;
3. }
4. method2 {
5. call method3;
6. }
7. method3 {
8. call readFile;
9. }
10.
还假如method1是唯一的能够处理readFile方法中所可能发生的错误的方法,那么传统的错误处理技术会强制method2和method3来传递通过readFile调用堆栈所返回的错误代码,直到错误代码传递到method1-因为只有method1能够处理这些错误,其代码结构如下所示:
1. method1 {
2. errorCodeType error;
3. error = call method2;
4. if (error)
5. doErrorProcessing;
6. else
7. proceed;
8. }
9. errorCodeType method2 {
10. errorCodeType error;
11. error = call method3;
12. if (error)
13. return error;
14. else
15. proceed;
16. }
17. errorCodeType method3 {
18. errorCodeType error;
19. error = call readFile;
20. if (error)
21. return error;
22. else
23. proceed;
24. }
25.
回忆一下,Java运行时环境搜寻调用堆栈来查找任意的处理特殊的异常的方法。一个方法能够抛出它内部的任何异常,所以允许一个上层调用堆栈的方法来捕获它。因此只有处理相关错误的方法来处理发现的错误,代码结构如下所示:
1. method1 {
2. try {
3. call method2;
4. } catch (exception e) {
5. doErrorProcessing;
6. }
7.
8. }
9. method2 throws exception {
10. call method3;
11. }
12. method3 throws exception {
13. call readFile;
14. }
15.
无论怎样,就像伪代码所展示的那样,躲避异常需要中间方法做一些工作。任意被检查到的由内部方法的抛出的异常必须在这个方法的throws子句中被指定。
优势3:分组和区分错误类型
因为所有在程序内部抛出的异常都是对象,异常的分组或分类是类继承的自然结果。在Java平台中一组相关异常类的例子是在java.io中定义的IOException和它的子类。IOException是最普通的IO异常管理类,并且它描述了在执行I/O操作时所发生的任意的错误类型。它的子类描述了一些特殊的错误。例如,FileNotFoundException异常类代表不能在本地磁盘上找到一个文件。
一个方法能够编写特殊的异常处理器,使它能够处理非常特殊的异常。FileNotFoundException异常类没有子类,因此下面的异常处理器只能处理一种异常类型:
catch (FileNotFoundException e) {
...
}
一个方法能够基于它的分组或通过在catch子句中所指定的任何异常的超类的一般类型来捕获异常。例如,要捕获所有的I/O异常,而不管它们的具体类型,就可以在异常处理器中指定一个IOException参数:
catch (IOException e) {
...
}
这个处理器将捕获所有的I/O异常,包括FileNotFoundException,EOFException等等。你能够通过查询传递给异常处理器的参数找到发生错误的详细信息。例如,打印堆栈执行路线:
catch (IOException e) {
e.printStackTrace(); // output goes to Sytem.err
e.printStackTrace(System.out); // send trace to stdout
}
你甚至可以创建一个能够处理任意类型的异常的异常处理器:
catch (Exception e) { // a (too) general exception handler
...
}
Exception类是Throwable类结构中的顶级类,因此,这个处理器将捕获除了那些被特定处理器捕获的异常以外的异常。你可能想你的程序是否都是这种处理异常的方法,例如,为用户打印错误消息并且退出。
但是,在大多数情况下,你需要异常处理器来尽可能的处理精确一些。原因是在处理器决定最好的恢复策略之前,必须做第一件是判断发生异常的类型是什么。在没有捕获特定错误的情况下,处理器必须有效的提供任意的可能性。Exception 处理器是最一般的异常处理器,使用这个处理器使得代码捕获和处理更多的程序员没有预料到的错误倾向,从而使得处理器没有目的性。
象我们展示的一样,你能够创建异常组,并且用一般化的方式来处理异常,或者使用特定异常类型来区分异常并且用精确的方式来处理异常。下一节中我们将介绍如何 捕获和处理异常。