这种异常必须在编译前就try/catch,又不一定会抛异常,小项目中不明显,大项目中,会造成不必要代码臃肿和可读性降低,完全可在编译出错时,通过单元测试和调试,得到正确代码。这设计还有啥意义?

Checked Exception初衷很好,但事实上是没啥卵用设计。

初衷很好

因为我们都知软件会有各种问题,严谨处理这些问题会很好提高健壮性。Checked Exception就是让一个方法指定自己一定会抛啥异常,调用者须决定一定要处理(catch),或明确声明继续向上抛(throws)。整个程序对异常处理就很明确,程序员也有章可循,UT,测试也能明确该处理啥错误。

现实骨感

若说较接近底层的系统还能相对设计出较完备严谨的异常体系,业务系统做这个严重吃力不讨好。

业务系统中,一个典型业务接口,有一个正常处理结果,但可能却有几十个不正常case。而Checked Exception要求须每层调用,要不挨个处理,要不挨个声明往上抛。

挨个处理

不现实,因为:

  • 大部分情况下,大多数异常都是极端情况,几乎不会出现
  • 即使出现,影响也不大

如一个基金详情,底层可能调用十几个不同基金信息数据源(基本信息,经理,基金公司,费率,净值,重仓股)。如因为意外少个1、2个,最好软处理。如须使用Checked Exception,很多人会这么干:

try {
  Fund fund = getFund(fundCode);
} catch (SomeNotImportantButCheckedException e) {
  // do nothing
}

用IDE自动产生catch block但就不处理。或干脆:

try {
  Fund fund = getFund(fundCode);
} catch (Exception e) {
  // do nothing
}

这比不catch,自动往上抛更垃圾。所以一般来讲,业务系统都会有收底的错误处理,这个处理可能在业务系统最高层。大部分不需要认真处理的异常往上抛。当真意识到某问题值得仔细处理,可能才专门为它仔细设计Exception和处理。

挨个明确[throws]也不现实。一般看throws后边能挂3~4种Exception。按上面假设,可能要搞几十种Exception,且随层级提高,数量越堆越多。Java这一般建议用类的体系来组织这些Exception,然后throws一个合适基类。但设计一个比较好的类体系很难。更何况大多数异常都不重要,直接收底处理的。

为不重要的事耗精力不值当,于是基本都简化处理,直接用 Exception、 RuntimeException (连throws也不用)或自己封装一个“BizException”包装错误码和相关信息。

业务变化剧烈呢?

上面这些还都是在设计时可定义所有异常的情况下遇到的问题。但业务剧烈变化时,不可能初始就预见所有可能问题。强行加Checked Exception对业务系统的接口,是不向前兼容的。

一旦真加了,所有调用方须被逼跟着改:

  • 一个系统内部,一个code base(代码库)可能还好处理
  • 对可能跨系统调用(如你写个jar,别人maven依赖),就可能灾难性。如某些组件因种种原因无法升级,就不能使用新代码!

因此,一个好的错误处理体系,最好满足:

  • 不会倒逼程序挨个处理无聊异常,允许程序员有选择将关注点放在哪些最关键问题
  • 如团队真要严谨错误处理,可提供一个有力支持,但这支持是团队根据开发的内容来决定使用,而非强制所有语言的使用者都遵循同一套“规则”
  • 允许错误处理渐进性的发展

Java异常最佳实践

基建团队

使用Checked Exception,并定义良好的异常继承体系,认真处理所有异常。有限度使用RuntimeException

业务团队

基于RuntimeException定义一个BizException描述各种业务问题,BizExcpetion包含错误码和错误描述信息;同时定义InteralServerError包装各种系统错误,如网络超时。输出http response时,二者输出不同,然后定义不同监控和告警机制

特定异常

由产研共同商讨设计:

  • 哪些可完全不管(如一个不关键数据拿不到),软处理
  • 哪些要前端用户知晓并处理(如登录时用户名或密码错误)
  • 哪些由程序尽量自己处理(如关注的某产品超时,后端要尝试重试几次)

其他语言处理异常

go用err(大致等价错误码,但可包含一些数据信息),因此异常可【不捕获而往上抛】的好处就得不到。这可解释为啥很多搞底层开发的不觉go没Exception而难用,因为反正错误都要严谨处理,Exception那点优势不重要。但即便如此,go也不强制调用者必须处理写if err != nil 。

但给go加Exception的呼声越来越强烈,应该是 Java 转 Go 的业务开发越来越多。

js也有Error,但不是很喜欢搞继承。因此javascript一般就只用“Error”,“TypeError”这种简单东西。因为动态语言,开发者可选择自己往Error里塞自己喜欢的东西,并用一些松散的约定解决问题。如:

throw Error("ERR_INVALID_PASSWORD");

简单的用字符串来定义错误。js主要场景在前端,这时出错:

  • 要不给用户展示错误信息

  • 要不用错误上报接口报给后端

复杂的异常体系也没用,更谈不上Checked exception。而服务端的NodeJS exception处理就能借鉴很多Java语法。

kotlin直接砍掉Checked Exception,但保留其他常规异常语法(改成Expression,用起来爽很多)。文档提供两个论据:

swift更有趣,认为[函数]的[异常模式]有两种:

  • 会抛出异常的,于是函数名后边要声明“throws”,但是不需要声明会抛啥异常
  • 肯定不会抛出异常的,所以实现中必须吃掉各种可能发生异常的情况

[编译器]会强制确保这个语义的正确。throws这种方式,大概等价于Java中直接throws Exception。

因此从工程角度和语言发展角度来讲,Checked Exception早已经被扔进了垃圾堆。在整个工程项目的错误处理体系里,它的作用已经越来越少。新的语言纷纷抛弃掉这个华而不实的设定。希望广大入场者只要知道Checked Exception是什么就好,实战时还是根据业务场景来设计错误处理。

结尾

也许还有人觉得Checked Exception是一种可以推进减少程序错误,提高健壮性的好措施。错的是懒惰的,不称职的程序员,而不是Checked Exception。但从我认为,如果一个措施不能有助于解决问题,反而加重问题,那就是无用的。不要把时间和精力浪费在无用的事物。