在这个开源框架大放异彩的时代,人们往往非常关注架构本身引入的各种新理念,而其异常处理机制常常被忽视。事实上,一个框架的异常处理方式是维持其稳定的基础。而且一个应用程序框架本身对异常的处理方式,也会对其使用者处理异常的方式造成深刻的影响。本文参考了一些大师对JAVA中异常使用方式的论述,并提出了几条简单的见解,作为抛砖引玉之作,请大家共同探讨。
在JAVA中,异常实际上可以看作是方法的非正常返回值。例如 User getUser(String usrName)这个方法,本来应该返回一个用户对象,但是当异常出现时,方法并没有返回User,而是返回了一个Exception。这个非正常情况下的返回值就是异常。
本着异常就是方法的非正常返回值这种思想,可以对以下几个问题进行探讨:
1. 什么时候应该使用(自定义)异常
2. 应该使用已检查异常(CheckedException,我认为如果翻译为可控异常更加恰当),还是运行时异常(RuntimeException)。
什么时候应该使用异常:
关于自定义异常使用的时机,有一种传播的较为普遍的说法:异常只能用于处理“真正的异常情况”,不能参与流程控制,因为过多使用异常会造成性能的损失,因此在我们的系统中,应该尽量少使用自定义异常。
首先让我们考察一下什么是“真正的异常”。
一般情况下人们认为在操作系统一级(准确地说是JDK一级)抛出的异常就是“真正的异常”。然而所谓“系统级异常”和“应用级异常”在很多时候很难界定。例如对于一个文字处理软件来说,“NoEnoughSpaceException”这个异常究竟是系统级的,还是应用级的?从系统的角度来看,磁盘空间不够,应属于系统级异常。但从应用的角度看,文件无法保存了,也是应用级异常。因此我认为这种分类方法从分类学角度来说是不科学的:所谓“系统级”和“应用级”的范围经常会互相覆盖。
另外一种较注重实效的看法是,从方法的功能角度看,决定是否应该使用异常。结合前面的“异常事实上是一种特殊返回值”的说法,举例如下:
User getUser(String usrName) ,使用者期望得到User对象,但是如果真的有特殊情况发生,而使用者又没有进行事先判断,那么我们只能抛出异常。在这里可能抛出的异常是一个自定义的异常:“用户不存在”。
boolean isUserExist(String usrName) ,使用者是在判断用户是否存在,所以我们当然没有必要抛出“用户不存在”异常,只需返回一个布尔值即可。
void registUser(String usrName),这个方法没有返回值,因此如果出现了异常情况,只能抛出异常。这里可能抛出的异常有“用户名已用”、“系统禁止注册”等。
我认为第二种使用异常的方法是较科学且较直观的:一切从实效出发。在这种做法的指导下,我们可能会在系统中使用大量自定义异常。
有人认为异常不应该参与流程控制,并将这种参与流程控制的异常讽刺为“SuperIf”。关于此问题我们可以参考RUP中关于异常事件流的论述, RUP的结论是“异常情况是现实中客观存在的一类事件,当我们必须对它进行处理时,这个处理就发生在异常流中”。因此在JAVA中将异常作为一种流程控制手段,和RUP的异常流概念不谋而合,且对异常的处理就可以在异常块中进行。
有人可能害怕大量使用异常会带来性能上的损失。性能问题是另外一个需要庞大篇幅论证的问题,大家可以参考Rod Johnson在他的《J2EE Without EJB》一书中对性能优化的分析。简而言之是一句话:一切语言级优化所作的努力,在一次I/O或者数据库读写操作中将化为乌有。因此对异常使用带来的小小性能损失,实在是不需“言必性能”。
使用已检查异常还是运行时异常:
这个问题的争议较大,比较极端的如
Bruce Eckel和Anders Hejlsberg,认为应该取消一切CheckedException,以带有自定义消息的RuntimeException替代之,
Bruce甚至亲自给出一段代码告诉大家如何将CheckedException转为RuntimeException。而James Gosling则使用CheckedException过了火,有人讽刺James:在他的眼里,用户都是上帝,有能力且必须处理诸如“IOException”这类谁也没有办法的事情。Rod Johnson比较中庸,他认为CheckedException有其有用之处:类似James Gosling说的,当用户必须对某种特殊情形作出反应的时候,我们可以使用CheckedException来提醒用户“这种情形可能会发生,你要对它进行处理”。
但是Rod极大的缩小了这种情形发生的范围,他认为只有当异常流必须被特殊处理的时候,才有使用CheckedException的需求。举例来说,int registUser(String usrName)方法用于注册用户。这个API的作者规定,当返回值为0时,代表注册成功;返回值为1,代表用户名已用;返回值为2,代表系统禁止注册…… 在这种情况下,就应该使用CheckedException,而不是RuntimeException或简单返回值。因为除了0的每个返回值都代表了一条异常流,且每个异常流都要进行特殊处理,比如用户名已有时,我们可能要自动转入一个提醒页面,提醒用户其他可用的用户名;禁止注册时,我们要进入一个公告页面,这个页面展示了禁止注册的原因及开放时间等等。
对上面的情况,可以做一个更精确的总结:只有当异常参与了流程控制时,才应该使用CheckedException。
那么我们如何判断异常是否应该参与流程控制?这种判断可以从需求入手。如果需求要求我们,在这条异常流上,要进行A处理,在那条异常流,要进行B处理,则很明显,这里应该使用CheckedException来控制流程。如果需求只是“如果发生了异常,在界面上进行提示”,那么这种情况下,我们就没有必要使用CheckedException,只要使用带有消息的RuntimeException即可。
使用CheckedException有一个显著的问题,因为每个使用者都必须对方法法抛出的CheckedException作出响应,因此一旦方法已经被广泛使用,要添加一个新的CheckedException就变得十分困难:每个使用者都要做出相应的修改。但是从另一个角度看,如果你添加CheckedException的理由确实是“需求要求如此,我们必须对它进行特殊处理”,则每个方法的使用者就应该增加一个新的catch,并增加这种新的特殊处理。
这个问题也提醒我们,使用CheckedException时,一定要确定是否这种情况需要“特殊处理”,否则尽量以RuntimeException进行代替。