使用异常的优势
现在你知道了异常是什么,并且知道怎么使用它们,现在是时候讨论一下在你的程序中使用异常会有什么好处了。
优势1:隔离错误处理代码和常规代码
Exception提供了一种方法,把意外发生时的细节从程序主逻辑中隔离开来。在传统的编程中,错误的检测、报告和处理通常会导致像意大利面条那么混乱的代码。例如,请看下面的伪码,它把整个文件读入内存:
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; }
有那么多的错误检测、报告和返回语句,让原本简单的5行代码完全迷失在乱哄哄的代码里面了。更糟糕的是,代码的逻辑流程也随着丢失了,因此很难说这段代码是否做了正确的事:当函数分配内存的时候失败了,文件能否确保被关闭?当你三个月之后再修改这段代码时,你就更难确定改完后代码还到底能不能用了。很多程序员解决的办法就是简单地忽略它——当他们的程序崩溃了报告一下错误就行。
Exception可以让你能在一个地方写好代码主流程,在另外一个处理异常情况。如果readFile函数改用Exception来替换传统的错误管理技术,看起来就像下面的代码:
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; } }
要注意的是,使用Exception并不会让你在检测、报告和处理错误时更省力,但是可以让你把这些工作组织起来更高效。
优势2:在调用栈中向上传播错误
Exception的第二个优势就是,可以把错误报告给调用栈的上层方法。假如在主程序中,在一系列嵌套的调用方法中,readFile方法是第四个方法: method1调用method2,然后调用method3,最后调用readFile。
method1 {
call method2;
}
method2 {
call method3;
}
method3 {
call readFile;
}
假设method1是唯一关心在readFile内发生了什么错误的方法。传统的错误通知技术强制method1和method2传播readFile返回的错误代码,直到错误代码最后传到method1——而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从句中进行声明,可以抛掉任何检查异常。
优势3:归类和区分错误类型
因为程序抛出的所有异常都是对象,类是有层次结构的,所以对异常进行归类或分组是非常自然的结果。在Java平台中有一个例子,一组相关异常被定义在java.io包——IOException及其子类。IOException是最通用的,它代表了我们在执行I/O相关操作的时候发生的任何错误类型。它的继承者代表了更加细分的错误。例如,FileNotFoundException表示一个文件不能再磁盘中定位到。
一个方法可以写一个具体的处理器,能处理一个十分具体的异常。由于FileNotFoundException没有继承者,所以下面的这个异常处理器只能处理一种类型的异常:
catch (FileNotFoundException e) { ... }
一个方法也可以用更通用的处理器捕获处理具体的异常。例如,为了捕获所有的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); }
你甚至可以构建一个可以处理所有异常的异常处理器:
// A (too) general exception handler catch (Exception e) { ... }
在Throwable类层级结构中,Exception类已经快接近顶点了。所以这个处理器将会捕获很多它不想捕获的异常。如果你想在整个程序中对所有异常都采取统一处理方式,你就可能会采取这种办法处理异常,例如,给用户打印错误信息并退出。
然而在大多数情况下,你希望异常处理器越具体越好。理由是在你决定最佳的恢复策略之前,你首先要知道错误的类型。事实上,如果不捕获具体的错误,这个处理器就必须要容纳任何可能性。太通用的异常处理器可能会让代码更容易出错,因为它们会捕获和处理程序员意料之外的异常,这样就超出处理器的能力范围了。
就像前面说过的,你可以创建一组异常,然后采用通用处理的风格。或者你可以使用具体的异常类型来区分不同的异常,然后用精确的风格处理异常。
转自:http://leaforbook.com/blog/jexception/translate/advantages.html