异常处理的关键就在于知道何时处理异常以及如何使用异常。
这篇文章,我会提到一些最佳的异常处理方法。
我也会总结checked exception 的用法。
我们程序员都想写出高质量的代码来解决问题。
但是,异常有时会给我们的代码带来副作用。
没有人喜欢副作用,所以我们很快找到了方法来改善它们。
我看见过许多java招聘问这样的问题,聪明的程序员通常这样来处理异常:1 2 3 4 5 6 7public void consumeAndForgetAllExceptions (){try {...some code that throws exceptions} catch (Exception ex){ex.printStacktrace ();}}上面的代码有什么错误?当异常被抛出后,正常的程序执行过程中断,控制权交给catch 段,catch 段会catch 异常,然后抑制异常的进一步扩大。
然后接着catch 段之后程序继续执行,好像什么都没发生过一样。
下面的代码呢?1 2public void someMethod () throws Exception{}这个方法内没有代码,是个空方法。
一个空方法怎么能抛出异常呢?Java 并没有说不让这么做。
最近,我遇到过类似的代码,方法抛出了异常,而其中的代码实际上并不产生那个异常。
当我问这个程序员Java异常为何要这么做,他回答道“我知道,虽然这样做破坏了API,但我习惯这么做,而且这样也可行。
”C++社区用了许多年才确定如何使用异常机制。
这个争论刚刚在Java 社区展开。
我见到一些Java 程序员正在和异常进行顽强抗争。
如果用法不当的话,会拖慢程序,因为创建、抛出和接住异常都会占用内存。
如果过多的使用异常的话,代码会变得很难阅读,对要使用API 的程序员来说无疑会增加挫败感。
我们知道挫败感会令我们写出很烂的代码。
有的程序员会刻意回避这个问题,忽略异常或随意抛出异常,就像上面的两个例子一样。
异常的本质广义的讲,抛出异常分三种不同的情况:- 编程错误导致的异常:在这个类别里,异常的出现是由于代码的错误(譬如NullPointerException 和IllegalArgumentException)。
代码通常对编程错误没有什么对策。
- 客户端的错误导致的异常:客户端代码试图违背制定的规则,调用API 不支持的资源。
如果在异常中显示有效信息的话,客户端可以采取其他的补救方法。
例如:解析一个格式不正确的XML 文档时会抛出异常,异常中含有有效的信息。
客户端可以利用这个有效信息来采取恢复的步骤。
- 资源错误导致的异常:当获取资源错误时引发的异常。
例如,系统内存不足,或者网络连接失败。
客户端对于资源错误的反应是视情况而定的。
客户端可能一段时间之后重试或者仅仅记录失败然后将程序挂起Java 异常的类型Java 定义了两种异常- Checked exception: 继承自Exception 类是checked exception。
代码需要处理API 抛出的checked exception,要么用catch 语句,要么直接用throws 语句抛出去。
- Unchecked exception: 也称RuntimeException,它也是继承自Exception。
但所有RuntimeException 的子类都有个特点,就是代码不需要处理它们的异常也能通过编译,所以它们称作unchecked exception。
图1 显示了NullpointerException 的继承级别。
图 1 异常等级实例NullpointerException 继承自RuntimeException,所以它是个unchecked exception。
我看到人们大量使用checked exception 的,而很少看到unchecked exception 的使用。
近来,在Java 社区里对checked exception 和它的真正价值的争论愈演愈烈。
这主要因为Java 是第一个使用checked exception 的主流面向对象语言。
C++和C# 都没有checked exception,所有的异常都是unchecked。
低层次抛出的checked exception 对高层次来说,必须要catch 或者throw 它们。
这样如果不能有效处理异常的话,checked exception 就在API 和代码之间造成了一直负担。
程序员就开始写一些空的catch 代码段,或者仅仅抛出异常,实际上,给客户端的触发者来说增加了负担。
Checked exception 也被诟病破坏了封装性。
看看下面的代码:1 2 3 4public List getAllAccounts () throwsFileNotFoundException, SQLException{...}getAllAccounts ()抛出了两个checked exception。
这个方法的调用者就必须处理这两个异常,尽管它也不知道在getAllAccounts 中什么文件找不到以及什么数据库语句失败,也不知道该提供什么文件系统或者数据库的事务层逻辑。
这样,异常处理就在方法调用者和方法之间形成了一个不恰当的紧耦合。
设计API 的最佳实践说了这么多,让我们来说说如何设计一个好的API,能够正确抛出异常的。
1. 当要确定是使用checked exception 还是unchecked exception 时,首先问问自己,当异常发生时客户端如何应对?如果客户端可以从异常中采取行动进行恢复的,就使用checked exception,如果客户什么也做不了,就用unchecked exception。
我指的是,不仅仅是记录异常,还要采取措施来恢复。
还有,我更喜欢unchecked exception,因为不需要强迫客户端API 必须处理它们。
它们会进一步扩散,直到你想catch 它们,或者它们会继续扩散爆出。
Java API 有许多unchecked exception 如NullPointerException, IllegalArgumentException 和IllegalStateException。
我更愿意用这些Java 定义好的异常类,而非我们自己创建的异常类。
它们使我们的代码易读,也避免代码消耗更多内存。
2. 保持封装性不要将针对某特定实现的checked exception 用到更高的层次中去。
例如,不要让SQLException 扩散到逻辑层去。
因为逻辑层是不需要知道SQLException。
你有两种选择:- 如果你的客户端有应对措施的话,将SQLException 转化成另一个checked exception。
- 如果你的客户端什么也做不了的话,将SQLException 转化成一个unchecked exception。
但大部分情况是,客户端对SQLException 无能为力。
那请将SQLException 转换成unchecked exception 吧。
来看下面的代码:1 2 3 4 5 6 7public void dataAccessCode (){try{..some code that throws SQLException}catch(SQLException ex){ex.printStacktrace ();}}上面的catch 段仅仅抑制了异常,什么也没做。
这是因为客户针对SQLException 无计可施。
何不使用下面的方法呢?1 2 3 4 5 6 7public void dataAccessCode (){try{..some code that throws SQLException}catch(SQLException ex){throw new RuntimeException (ex);}}将SQLException 转换成RuntimeException。
如果SQLException 发生时,catch 语句抛出一个新的RuntimeException 异常。
正在执行的线程会挂起,异常爆出来。
然而,我并没有破坏逻辑层,因为它不需要进行不必要的异常处理,尤其是它根本不知道怎么处理SQLException。
如果catch 语句需要知道异常发生的根源,我可以用getCause ()方法,这个方法在JDK1.4 中所有异常类中都有。
如果你确信逻辑层可以采取某些恢复措施来应对SQLException 时,你可以将它转换成更有意义的checked exception。
但我发现仅仅抛出RuntimeException,大部分时间里都管用。
3. 如果自定义的异常没有提供有用的信息的话,请不要创建它们。
下面的代码有什么错误?public class DuplicateUsernameExceptionextends Exception {}它没有给出任何有效的信息,除了提供一个异常名字意外。
不要忘了Java 异常类就像其他的类一样,当你在其中增加方法时,你也可以调用这些方法来获得更多信息。
我们可以在DuplicateUsernameException 中增加有效的方法,例如:1 2 3 4 5 6 7public class DuplicateUsernameExceptionextends Exception {public DuplicateUsernameException(String username){....}public String requestedUsername (){...}public String[] availableNames (){...}}新版本的DuplicateUsernameException 提供两个方法:requestedUsername ()返回请求的姓名,availableNames ()返回与请求姓名相类似的所有姓名的一个数组。
客户端可以知道被请求的姓名已经不可用了,以及其他可用的姓名。
如果你不想获得其他的信息,仅仅抛出一个标准的异常即可:1 throw new Exception ("Username already taken");如果你认为客户端不会采取任何措施,仅仅只是写日志说明用户名已存在的话,抛出一个unchecked exception:1 throw new RuntimeException ("Username already taken");另外,你甚至可以写一个判断用户名是否已经存在的方法。
还是要重复一遍,当客户端的API 可以根据异常的信息采取有效措施的话,我们可以使用checked exception。