1.有效处理java异常三原则
java中异常提供了一种识别及响应错误情况的一致性机制,有效地异常处理能使程序更加健壮,易于调试。异常之所以是一种强大的调试手段,在于其回答了以下三个问题:
- 什么出了错?
- 在哪里出错?
- 为什么出错?
有三个原则可以帮助你在调试过程中最大限度的使用好异常:
- 具体明确
- 提早抛出
- 延迟捕获
通过杜撰个人财务管理器类JCheckbook进行讨论,JCheckbook用于记录及追踪诸存取款,票据开具之类的银行账户活动。
1.1具体明确
java定义了一个异常类的层次结构,以Throwable开始,扩展出Error和Exception,而Exception又扩展出RuntimeException。
图一,java异常层次结构
这四个类是泛化的,并不提供出错信息,虽然实例化这几个类语法上合法(如:new Throwable()),java提供了大量异常子类,如果贴近业务,你也可以定义自己的异常类。
1.1.1捕获异常时尽量明确很重要。java让明确捕获异常变得容易,因为我们可以对同一try块定义多个catch块,从而对每一种异常分别进行恰当处理。
File prefsFile = new File(prefsFilename); try{ readPreferences(prefsFile); } catch (FileNotFoundException e){ // alert the user that the specified file // does not exist } catch (EOFException e){ // alert the user that the end of the file // was reached } catch (ObjectStreamException e){ // alert the user that the file is corrupted } catch (IOException e){ // alert the user that some other I/O // error occurred }
最后,我们注意到JCheckbook并没有在readPreferences()中捕获异常,而是将捕获和处理异常留到用户界面层来做(Controller层),这样就能用对话框或其他方式来通知用户。这被称为“延迟捕获”。
1.2提早抛出
异常堆栈信息提供了导致异常出现的方法调用链的精确顺序,包括每个方法名调用的类名,方法名,代码文件名甚至行数,以此来精确定位异常出现的现场。
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)
通过逐步回退跟踪堆栈信息并检查代码,我们可以确定错误原因是向readPreferences()传入了一个空文件名参数。
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... }
通过提早抛出异常(又称"迅速失败"),异常得以清晰又准确。堆栈信息立即反映出什么出了错(提供了非法参数值),为什么出错(文件名不能为空值),以及哪里出的错(readPreferences()的前部分)。
这样我们的堆栈信息就能如实提供:
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)
通过在检测到错误时立刻抛出异常来实现迅速失败,可以有效避免不必要的对象构造或资源占用,比如文件或网络连接。同样,打开这些资源所带来的清理操作也可以省却。
1.3延迟捕获
捕获之后该拿异常怎么办?最不应该的就是什么都不做。适当分离用户界面代码和程序逻辑就可以提高我们代码的可重用性。
在有条件处理异常之前过早捕获它,通常会导致更严重的错误和其他异常。
例如: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中恢复过来的情况下就捕获了它。如果文件无法找到,下面的方法显然无法读取它。
调试程序时,本能告诉我们要看日志最后面的信息。那将会是NullPointerException,非常让人讨厌的是这个异常非常不具体。错误信息不仅误导我们什么出了错(真正的错误是FileNotFoundException而不是NullPointerException),还误导了错误的出处。真正 的问题出在抛出NullPointerException处的数行之外,这之间有可能存在好几次方法的调用和类的销毁。
结论【抛异常】:既然readPreferences() 真正应该做的事情不是马上捕获这些异常,把责任交给 readPreferences()的调用者,让它来研究处理配置文件缺失的恰当方法,它有可能会提示用户指定其他文件,或者使用默认值,实在不行的话也 许警告用户并退出程序。把异常处理的责任往调用链的上游传递的办法,就是在方法的throws子句声明异常。
当然,你的程序最终需要捕获异常,否则会意外终止。但这里的技巧是在合适的层面捕获异常,以便你的程序要么可以从异常中有意义的恢复并继续下去,而不是导致更深入的错误;