我写了八年Java,前三年无比坚信受检异常是Java最伟大的发明——编译器强迫你处理异常,这难道不是负责任吗?直到我维护一个老旧的Spring Boot服务,被一个藏在try-catch里的SQLException搞到凌晨三点,才猛然发现自己被这个“负责任”的设计骗了:调用方根本不知道catch之后该干嘛,只能打一行日志然后重新抛出一个没有语义的RuntimeException。
举个最日常的例子:你写一个方法解析文件,里面声明throws IOException。OK,调用方被迫处理异常。但问题是——文件不存在时,调用方真正需要的可能只是告诉用户“你选的路径无效”,可受检异常逼着他先catch IOException,再自己包装成业务异常。如果哪天底层换成读取HDFS,抛出的从IOException变成了Hadoop的异常(那是个RuntimeException),那么所有调用方都不用改代码,反而那个被强迫签名的IOException变成了永远不可能发生的幽灵分支。这种“强制”带来的不是安全,而是无意义的负担。
我在C#里待了两年,C#的异常体系直接没有受检异常这回事。起初我鄙视这种“不负责任”的做法,后来我发现C#开发者们处理异常的方式更接近现实:只在能真正处理异常的边界捕获,其他情况让异常坦诚地沿着调用栈上抛,由统一的全局处理器兜底。而Kotlin干脆在语言层面把受检异常去掉了,因为JetBrains的团队做过调研,大多数开发者只是在catch块里打印日志,然后继续抛出。你看,不是开发者懒惰,而是受检异常这个设计本身假设了“调用方有足够信息去处理”,但真实世界不是这样的——绝大多数底层IO异常,到了业务层根本没有恢复的可能,真正的处理方式只能是进行重试、降级、或直接返回错误码。你让每一层都增加一个throws声明,等于把这种“无法处理”的窘境在签名里反复唱了三遍。
当然,我不认为受检异常应该被彻底扔进垃圾桶。真正的问题在于Java社区从来没有人教我们怎么用对受检异常。Effective Java里建议受检异常用于可恢复的条件,但什么算可恢复?这个判断标准太模糊了。我见过太多项目把受检异常当作一种“流程控制”——比如用户已存在就抛一个DuplicateKeyException,调用方捕获后弹出提示。这种用法滥用Java想要表达的语义直接导致异常变成控制流的暗号,类与类之间耦合得死死的。我尝试过一种折中方案:只在模块边界使用受检异常,模块内部一切异常全部包装成领域专用异常并做成Unchecked。这样做的好处是,底层持久层换了、文件系统换了,上层业务代码感知不到signature的剧变;而在模块对外暴露的API上,受检异常明确告诉你“这里真的会失败,你必须有Plan B”,比如PayPal支付回调的时候,网络抖动可能会造成扣款成功但回调通知失败,你作为服务方必须设计重试和补偿机制。
所以我的最终观点是:受检异常并非设计失误,失误的是Java社区一直鼓励你到处声明throws,却忘了声明throws的代价——它让接口的演进僵化,让异步编程丑陋到极点。你看在CompletableFuture里,受检异常被塞进CompletionException变成了RuntimeException,Java官方用脚投票已经承认了这一点。如果你还在纠结要不要用受检异常,我建议你想清楚一个核心问题:你正在写的这个方法,到底能不能让调用方在捕获它之后,做出一个有实质意义的选择?如果答案是不确定,那就别声明throws,让它静默地成为一个Unchecked异常,别用你“负责任”的假象去绑架别人。