Java Exception 历史回顾

前言

本文从 Exception 的起源讲起,试图帮开发者建立对 Java、Kotlin 编程语言异常处理机制的感性认知。文章内容出自个人理解,如有纰漏,还请各位看官斧正。

1990 年前后, Sun 公司的工程师团队在项目开发中经历了 C++ 的诸多痛点,对此 Sun 公司内部成立了 Stealth 计划小组(后改名为 Green 计划小组)来解决这些痛点问题。小组起初进行了多种尝试:有切换编程语言到 NexT 的;也有尝试改进和扩展 C++ 的,但都宣告失败。最终小组内的 James Gosling ( Java 之父) 决定创造一个新的编程语言来解决问题。他以办公室外的橡树为名,把这一编程语言称为 Oak,随后因商标原因将其更名为 Java。 Java 语言的开发过程中广泛借鉴了 C/C++ 的特性,C++ 中的异常处理机制也被沿用了下来。

C++ 语言中的异常

异常:表示程序中意料之外,情理之中的一类错误。

C++ 中的异常处理是一种条件转移机制,当程序在运行过程中遇到 throw 关键字时,会中止当前的程序代码块并跳转到最近的 catch 代码块执行后续操作。这一行为对 C 语言中的 goto 机制,以及汇编语言中的转移指令(如:jmp)。

C++ with ExceptionC/C++ with Goto
exception-in-cppgoto-in-cpp

在 C 语言体系中,goto 因过于灵活和难以调试,常被诟病为邪恶的。常见的规避手段是使用 int 类型的返回值表示函数的执行状态,通常用 0 表示执行成功,非0表示执行失败。但上述做法违背了函数返回值的用途,导致大量库函数不得不增加额外的形参作为真实的返回值,如下所示:

1
2
3
4
// doSomething 函数
//   如果 doSomething 执行成功,则返回0,同时为 &result 赋值;
//   否则返回其他值,此时 &result 不可用。
int doSomething(char** param, Result* result)

C++ 引入的 try/catch/throw 异常处理机制极大的改善了 C 语言编程中错误处理的体验,因此得到了业界的接纳。

Java 语言中的异常

Java 语言在设计时延用了 C++ 中的异常处理机制,与 C++ 不同的是它扩展了 Exception 的概念1,把 Exception 进一步细分为 Checked ExceptionUnchecked Exception,前者是编译器可以识别的异常,后者是无法被编译器感知的异常。

  1. Checked Exception(受检异常)
    • 要求程序员必须对此类异常进行处理,如在代码块中采用 try/catch/finally包裹或在方法签名中添加throws关键字
  2. Unchecked Exception(非检异常)是
    • 不强制要求程序员对此类异常进行处理
    • 非检异常有两种,一种是 RuntimeException 与其子类;另一种是 Error 与其子类。

1. Checked Exception

Checked Exception 称为受检异常,它是一种编译器可以识别到的异常,当在代码中声明此类异常时,编译器和 IDE 会给出显式提示,如下所示

错误类型图示
编译器错误提示checked-exception-1
IDE 错误提示checked-exception-2

对于受检异常,可以参照下述方式解决

解决方式图式
在函数内进行try/catch异常捕获,避免影响调用方try-catch-exception
在函数外部声明 throws 关键字,提醒函数调用方进行异常捕获throws-exception

2. UnChecked Exception

Unchecked Exception 可称为非检异常,它是一种无法被编译器识别到的异常,当在代码中声明此类异常时,编译器和 IDE 不会给出提示,只在程序运行时才能发现,例如常见的 RuntimeException

无错误提示图示
IDE 无错误提示unchecked-exception-1
编译期无错误提示unchecked-exception-2
运行时报错unchecked-exception-3

Java 中 Checked Exception 的问题

Java 中引入的 Checked Exception 出发点很好,意图增强程序员在编写代码时对异常的感知能力,提醒程序员及时处理潜在的异常情况。但在实践了几年后,Java 社区中出现了反对这一设计的声音23

主流的反对声音有以下几点

  1. 不利于版本迭代:

    • SDK 升级过程中,若在此前的公开方法的 throws语句中添加了新的受检异常类型,则会导致 SDK 用户必须修改代码并重新编译才能完成 SDK 的升级
  2. 不利于代码的伸缩性:

    • 在项目开发中,很多每个方法都会throws一些特定的受检异常,包装这些方法时,需要在此基础上再次向上 throws

    • 假如有两个方法:模块1和模块2,以及一个中间件 M。其中模块 1 抛出了 10 个受检异常,模块 2 抛出了与前者不同的 8 个异常,那么组合这两个方法的中间件 M,则至少需要抛出 18 个受检异常。对于大型项目,这个数量会指数增加,就像一只失去控制的气球。

      中间件通常是用来将多个模块组合为一个可服用的单元,它对各个模块的受检异常并不感兴趣,也不会尝试去处理这些异常,然而受检异常却需要中间件参与异常的处理,这增加了中间件的职责。

  3. 无意义的模版代码:

    • 开发人员通常不在意受检异常的类型,他们会使用最基础的 Exception 来捕获所有异常,很多时候 catch 代码块甚至是空的。

基于上述原因,Checked Exception 在面向对象的其他语言中并没有得到大范围推广,例如 Ruby、C#、Kotlin 等 Java 的后继者们都摒弃了 Checked Exception 这一特性。

Kotlin 语言中的异常

与 C# 一样,Kotlin 团队认为所有异常都应是 Unchecked Exception,由最终用户负责异常的检测和捕获,因此在设计 Kotlin 时有意规避了 Java 中 Checked Exception,并取消了方法签名中的 throws 关键字。与此同时,为了 Kotlin 与 Java 的互操作性,Kotlin 提供了 @Throws 注解来为其他 JVM 上的语言的方法生成 throws 关键字,如下图所示。

exception-in-kotlin

小结

通过回顾 Exception 的历史,我们了解到以下内容:

  1. Exception 产生的原因
  2. Java 对 Exception 的扩展:Checked Exception
  3. 业界对 Checked Exception 的批评以及 Kotlin 语言对 Checked Exception 的摒弃

如何看待 Checked Exception是一个见仁见智的问题,在实际开发中还需要从特定编程语言的设计角度出发,结合项目情况以及团队情况,在组织内部制定统一的异常处理模型,方可最大化的利用编程语言中 Exception 的这一特性。

感谢您的阅读,如果您对 Exception 有其他的见解,欢迎在评论区留言讨论。


  1. https://docs.oracle.com/javase/specs/jls/se11/html/jls-11.html ↩︎

  2. https://radio-weblogs.com/0122027/stories/2003/04/01/JavasCheckedExceptionsWereAMistake.html ↩︎

  3. https://www.artima.com/articles/the-trouble-with-checked-exceptions ↩︎