Java异常的选择:Checked Exception还是Unchecked Exception ?

曾经听到过关于老司机和新手程序员的区别,其中最大的一个区别就在于异常的处理。新手程序员总是天真得把世界想得太美好,基本上没想过会出现异常的情况,而一个经验丰富的老司机会把最坏的打算考虑进去,给出相应的解决办法,使得发生异常时对系统的影响降低到最小。对此,我深表认同。现实的情况总是复杂的,而且还有很多不怀好意的人时刻准备攻击你的系统。使用你系统的用户越多,这种潜在的风险也就越大。

异常处理是应对这些风险的最强有力的武器。在Java的世界里,异常有两种:受检异常(checked exception)和非受检异常(unchecked exception)。想必所有的Javaer都使用过这两种异常,但是何时使用哪个异常缺失经常困扰程序员的头疼问题。在此,我分享一下自己的看法,如果你有不同的意见,请留意探讨。

1.如果正常情况下会出现,那么使用Checked Exception;反之,则使用Unchecked Exception

这条准则是我在决定使用Checked Exception还是Unchecked Exception的第一原则。如果API的使用者在正常使用的过程中都会出现异常,那么这种异常就属于Checked Exception。因为这种异常时属于程序执行流程众多分支之一,API的使用者必须意识到这种情况,并做出相应的处理。

举个栗子:

我希望向zookeeper中创建一个节点,那么这种情况就隐含了两个前提条件:

  • 父节点已经被创建(如果有的话)
  • 本节点还未被创建

那么,这个API的签名大致应该是这样:

1
void createNode(String path,byte[] data) throws FatherNodeNotExist, NodeExist;

API的使用者看到这个签名的定义时就会得到一个强烈的心理暗示,我需要考虑父节点不存在和本节点已存在的情况,那么他就不得不显示的去处理这两种异常。

有的朋友可能会争论说,我正常的情况下不会出现这种情况,因为使用这个API的前提就是先创建好父节点,而后创建本节点,那我就不用抛出两种异常了,使用者也轻松了许多。但事实真的如此吗?我们想当然的认为了使用者是自己人,他们会乖乖的按照我们的想法去先创建父节点,再创建本节点,如果是在一个很局限的使用场景下,每个人都说经过严格培训的,那么你可以去做这样的假设,但是我还是不推荐你这么做,因为这样设计使得系统是脆弱的,不稳定的。如果能通过系统能自己避免这些错误,为什么不呢?况且,如果你把这个API开放给第三方的使用者,那么情况会更糟糕,你根本不知道他们会怎样去使用API,这非常恐怖!

有时候情况会变得很复杂,正常情况的鉴定变得很困难,你肯定会遇到这种时候,此时就需要结合你的业务场景去权衡其中的利弊。这依赖与你的经验和对业务场景的理解,我无法给你一个绝对的建议,那样是不负责任的。

我再举个常见的栗子:用户修改他拥有的资源信息。在菜谱APP中给出一个接口,让用户修改他菜谱的信息。那么这里一个隐含的条件就是用户修改他自己的菜谱信息,他是无权限修改别人的菜谱信息的。那么这个API的签名可能是这样的:

1
void updateMenu(long menuId,long uid,String title,String description...);

如果用户尝试去修改不属于他的菜谱呢?我们是否需要throws UserPermissionException之类受检异常?我认为是不需要的。判断是这属于正常情况吗?我认为这不算是正常情况。
正常情况下,客户端调用修改信息的接口,那么menuId一定是属于这个用户的。如果出现这种情况,要么是你系统设计的就有问题,要么就是不怀好意的人在破坏你的系统。前者需要重新设计我们的系统,而后者我们更不用关系,直接抛出一个RuntimeException就可以,因为他不算正常用户。

2. 调用者中能从异常中恢复的,推荐使用受检异常;反之,则使用非受检异常

注意这里的一个关键词是推荐,决定使用哪种异常最为根本的还是第一条原则。如果第一条原则难以判断时,才考虑调用者。这条原则和Effective Java中的第58条很像,如果有这本书的朋友可以再拿出来读读。

我和Effective Java#58不同的观点在于,这条原则只能是推荐,另外,对于所有不能恢复的情况我都建议使用非受检异常。我对可恢复的理解是,如果API的调用者能够处理你抛出的异常,并给出积极的响应和反馈,并指导它的使用者做出调整,那么这就是可恢复的。不可恢复就是API的调用者无法处理你抛出的异常,或者仅仅只是打个LOG记录一下,不能对它的使用者做出提示,那么都可认为是不可恢复的。

还是最开始的栗子,如果调用createNode的调用者能响应FatherNodeNotExist,并把这种情况反应到终端上,那么使用受检异常是有积极意义的。对于不可恢复的情况,包括编程错误,我建议都是用非受检异常,这样系统能fail fast,把异常对系统的影响降到最低,同时你还能获得一个完整的异常堆栈信息,何乐而不为呢?!

基本上,这两条原则就能帮你决定到底是使用受检异常还是非受检异常了。当然,现实的情况很复杂,需要根据你所处的具体业务场景来判断,经验也是不可或缺的。在设计API的时候多问下自己这是正常情况下出现的吗,调用者可以处理这个异常吗,这会很有帮助的!

异常处理是一个非常大的话题,除了选择checked exception还是unchecked exception以外,还有一些一般的通用原装,例如:

  • 只抛出与自己有关的异常
  • 封装底层异常
  • 尽量在抛出异常的同时多携带上下文信息

这些在Effective Java中都有详细的介绍,朋友可以认真读一下这本书,写的非常好!

对异常处理有不同理解的朋友可以给我留言,一起讨论,共同进步!

参考文献:

Effective Java, 2nd Edition.pdf)

坚持原创技术分享,您的支持将鼓励我继续创作!