额外的编写多个Catch块,在某些情况下,可能是多余的负担,但是在这个例子中,它确实可以使程序以一种更加友好的方式回应程序发生的各种错误。
如果是IOException而不是我们起初所指定的那三个异常被抛出了,那么最后一个Catch块将处理并提供一个一般意义上的错误信息。这样,程序在提供某些具体的错误信息的同时也可以对意想不到的那些与文件有关的异常提供一般意义上的处理。
有时候,开发人员直接捕获Exception,然后显示异常类的名字和Stack Trace信息,但为了具体问题具体处理,请不要这样做。看到屏幕上的Java.io.EOFException或者Stack Trace信息可能会使用户迷惑,而不是帮助他。捕获具体的异常从而用英语或其它语言给用户一个与问题相关提示,同时将异常的Stack Trace放到LOG文件中。异常和Stack Trace对开发人员意味着一个有力的调试工具,而对用户却毫无用处。
最后,请注意到Jcheckbook将捕获和处理Exception类型异常推迟到用户界面上,而不是放到readPreferences()函数里。这样在界面上可以以对话框的形式提示的用户相关信息或者使用其它的处理方式。这就是我们待会儿将会讨论的“晚捕获”原则。
Translator comments:
所谓具体化为:
定义具体的代表某个特定错误的异常类。
方法声明特定的异常类。
方法捕获具体类。
目地:具体问题具体处理。
早抛出
通过
Stack trace向我们展示的引起异常的方法调用顺序、类名、方法名、原代码文件名以及每个方法调用的行号,这样可以帮助我们精确地定位异常发生的地方。考虑下面的Stack trace信息:
Java.lang.NullPointerException at Java.io.FileInputStream.open(Native Method) at
Java.io.FileInputStream.<init>(FileInputStream.Java:103) at
jcheckbook.JCheckbook.readPreferences(JCheckbook.Java:225) at
jcheckbook.JCheckbook.startup(JCheckbook.Java:116) at
jcheckbook.JCheckbook.<init>(JCheckbook.Java:27) at
jcheckbook.JCheckbook.main(JCheckbook.Java:318)
这表明类FileInputStream的open方法抛出一个NullPointerException异常,但注意到
FileInputStream.open()是Java标准类库的一部分。这样引起异常的原因很可能在我们自己
代码里面,而不Java
API,所以问题一定出在这之前的某个方法内,幸运的是它被显示了出来。
很不幸,异常NullPointerException恰好是Java中能提供有效信息最少的异常类中的一个。
它并不能告诉我们真正想知道的。同时我们也需要向后追踪去找到错误发生地。
通过向后追踪Stack
trace和检查我们的代码,我们发现错误是由于调用方法readPreferences()时传入的文件名
为null,因为方法知道一个空文件名将使方法不能再执行下去,所以它立即检查这个条件:
public void readPreferences(String filename) throws IllegalArgumentException
{ if (filename == null) { throw new IllegalArgumentException ("filename is
null"); } //if
//...perform other operations...
InputStream in = new FileInputStream(filename);
//...read the preferences file... }
因为比较早的抛出了异常,所以异常就变得更加具体和准确了。Stack
trace也很准确地反映出发生了什么异常,为什么及在什么地方。这样使得Stack
trace更加准确的反映了本来程序所发生的一切:
Java.lang.IllegalArgumentException: filename is null at
jcheckbook.JCheckbook.readPreferences(JCheckbook.Java:207) at
jcheckbook.JCheckbook.startup(JCheckbook.Java:116)at
jcheckbook.JCheckbook.<init>(JCheckbook.Java:27)at
jcheckbook.JCheckbook.main(JCheckbook.Java:318)
另外,异常信息(“filename is null”)指出了是什么为空,从而使得异常附带更多有用的信息。而这些是我们无法从异常NullPointerException中获得的。一旦出错误就立即抛出异常,这样可以避免再去构造或找开那些不再需要的对象或资源。比如说文件或网络连接。与打开这些资源相关的清理工作也可以避免了。
晚捕获
许多Java开发人员,无论新手或老手都普遍地犯一个错误就是在程序有能力处理一个异常之前就将它捕获了。Java编译器坚持让Checked Exception不是要被捕获就一定要在函数头中声明的作法也客观上促使了程序员采用上面这一错误作法。对程序员来说,一个很自然的趋势就是让代码包括在try程序块中并捕捉异常以阻止编译器报错。
问题是当捕获到异常之后你该如何处理它了?最坏的事情就是不做任何处理。一个空的catch块使异常无端地消失了从而使得关于异常的What、Where、Why信息永远丢失了。将异常的信息Log下来使情况稍好点,毕竟那样还有关于异常信息的记录。但是我们不可能期望用户想看甚至看懂Log文件和Stack trace信息。在函数readPreferences()内显示异常信息对话框是不合适的。因为Jcheckbook目前运行为桌面程序,我们也计划将其改写为基于HTML或者C/S的版本。
功能从Server上获取,而错误需要在浏览器或客户端中显示。我们应该带着为未来着想的想法去设计readPreferences()方法。恰当的将用户交互代码从程序逻辑中分开可以增加代码的可重用性。
在有能力处理一个异常之前去捕获它,常常会进一步地引起其它的错误和异常。比如,像readPreferences()方法立即捕获并处理了在调用FileInputStream构造函数所产生的FileNotFoundException异常,代码如下:
public void readPreferences(String filename)
{
//..
InputStream in = null;
// DO NOT DO THIS!!!
try
{
in = new FileInputStream(filename);
}
catch (FileNotFoundException e)
{
logger.log(e);
}
in.read(...);
//...
}
这段代码在对恢复错误无能为力的情况下捕捉了异常FileNotFoundException。如果文件没有找到,剩下的方法体肯定执行不起来。用一个不存在的文件名去调用方法readPreferences ()将会发生什么?当然,异常FileNotFoundException将会被Log,如果我们恰好去检查Log文件,我们将会意识到异常的发生。但如果程序继续去读那个文件当中的数据,那会发生什么了?因为文件不存在,in是null,所以一个NullPointerException将会被抛出。
当是调试程序时,直觉会使我们在日志文件中去检查最近的信息,你会非常恐惧的看到一个NullPointerException异常,因为它太一般了,不能提供你有价值的对判断异常发生原因有帮助的任何信息,Stack trace也是,这不仅使你无法了解错误是什么(真的错误是FileNotFoundException而不是 NullPointerException),而且也无法了解错误发生的正确地点。
除了catch异常外,readPreferences()方法还能对异常做何处理了?可能跟我们的直觉相反,仅仅将它上抛就可以了,不要立即catch异常。将处理的责任上抛给readPreferences()的调用者,让它去决定采用合适的方法去处理文件引用丢失的问题,它可以提示用户要求另一个文件、使用默认的值或者如果没有其它的办法,提示用户有问题发生并退出程序的运行也是可以。
这种将异常处理的任务上抛给它的调动链上的方法就是用throws关键字在方法头声明这些需要上抛的异常。当声明这些异常时,请尽可能使用能代表具体问题的异常类。这样可以使调动你方法的程序可以预料到到底会发生哪些异常并根椐具体情况处理它们。将前面代码修改如下:
public void readPreferences(String filename)
throws IllegalArgumentException,
FileNotFoundException, IOException
{
if (filename == null)
{
throw new IllegalArgumentException
("filename is null");
} //if
//...
InputStream in = new FileInputStream(filename);
//...
}
从技术上说,我们唯一需要声明的就是IOException,但是通过声明此方法可能会抛出FileNotFoundException可以帮助调用者更好的处理异常。而异常IllegalArgumentException是不需要声明的,因为它是一个Unchecked Exception(为RuntimeException的子类)仍然将其包括在其中是因为这样使代码更清晰[特别对调用者而言]。
当然,最终你的程序仍需捕获异常,要么程序就会意外地中止。但这个原则的意义就在于,它可以让你在合适地方捕获异常并能够提供合适的异常处理方法,不会再引起其它的异常,从而让程序能继续运行。或者提供用户一些具体有用的信息,包括如何从错误中恢复过来一些指示。当一个方法对异常不能做这两者之一时,简单地将异常上抛,这样它就会在合适的地方被捕获并处理。
总结
有经验的开发人员都知道调试程序最难的部分不是修改Bug,而是找出Bug的藏身之处。通过使用上面所讲的三个原则,你就可以使异常帮助你追踪和清除Bugs并且使得你的程序更加健壮和友好。