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 Exception | C/C++ with Goto |
---|---|
在 C 语言体系中,goto
因过于灵活和难以调试,常被诟病为邪恶的。常见的规避手段是使用 int
类型的返回值表示函数的执行状态,通常用 0
表示执行成功,非0
表示执行失败。但上述做法违背了函数返回值的用途,导致大量库函数不得不增加额外的形参作为真实的返回值,如下所示:
|
|
C++ 引入的 try/catch/throw
异常处理机制极大的改善了 C 语言编程中错误处理的体验,因此得到了业界的接纳。
Java 语言中的异常
Java 语言在设计时延用了 C++
中的异常处理机制,与 C++
不同的是它扩展了 Exception 的概念1,把 Exception 进一步细分为 Checked Exception 和 Unchecked Exception,前者是编译器可以识别的异常,后者是无法被编译器感知的异常。
- Checked Exception(受检异常)
- 要求程序员必须对此类异常进行处理,如在代码块中采用
try/catch/finally
包裹或在方法签名中添加throws
关键字
- 要求程序员必须对此类异常进行处理,如在代码块中采用
- Unchecked Exception(非检异常)是
- 不强制要求程序员对此类异常进行处理
- 非检异常有两种,一种是
RuntimeException
与其子类;另一种是Error
与其子类。
1. Checked Exception
Checked Exception 称为受检异常,它是一种编译器可以识别到的异常,当在代码中声明此类异常时,编译器和 IDE 会给出显式提示,如下所示
错误类型 | 图示 |
---|---|
编译器错误提示 | |
IDE 错误提示 |
对于受检异常,可以参照下述方式解决
解决方式 | 图式 |
---|---|
在函数内进行try/catch 异常捕获,避免影响调用方 | |
在函数外部声明 throws 关键字,提醒函数调用方进行异常捕获 |
2. UnChecked Exception
Unchecked Exception 可称为非检异常,它是一种无法被编译器识别到的异常,当在代码中声明此类异常时,编译器和 IDE 不会给出提示,只在程序运行时才能发现,例如常见的 RuntimeException
无错误提示 | 图示 |
---|---|
IDE 无错误提示 | |
编译期无错误提示 | |
运行时报错 |
Java 中 Checked Exception 的问题
Java 中引入的 Checked Exception 出发点很好,意图增强程序员在编写代码时对异常的感知能力,提醒程序员及时处理潜在的异常情况。但在实践了几年后,Java 社区中出现了反对这一设计的声音23。
主流的反对声音有以下几点
不利于版本迭代:
- SDK 升级过程中,若在此前的公开方法的
throws
语句中添加了新的受检异常类型,则会导致 SDK 用户必须修改代码并重新编译才能完成 SDK 的升级
- SDK 升级过程中,若在此前的公开方法的
不利于代码的伸缩性:
在项目开发中,很多每个方法都会
throws
一些特定的受检异常,包装这些方法时,需要在此基础上再次向上throws
;假如有两个方法:模块1和模块2,以及一个中间件 M。其中模块 1 抛出了 10 个受检异常,模块 2 抛出了与前者不同的 8 个异常,那么组合这两个方法的中间件 M,则至少需要抛出 18 个受检异常。对于大型项目,这个数量会指数增加,就像一只失去控制的气球。
中间件通常是用来将多个模块组合为一个可服用的单元,它对各个模块的受检异常并不感兴趣,也不会尝试去处理这些异常,然而受检异常却需要中间件参与异常的处理,这增加了中间件的职责。
无意义的模版代码:
- 开发人员通常不在意受检异常的类型,他们会使用最基础的
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 的历史,我们了解到以下内容:
- Exception 产生的原因
- Java 对 Exception 的扩展:Checked Exception
- 业界对 Checked Exception 的批评以及 Kotlin 语言对 Checked Exception 的摒弃
如何看待 Checked Exception是一个见仁见智的问题,在实际开发中还需要从特定编程语言的设计角度出发,结合项目情况以及团队情况,在组织内部制定统一的异常处理模型,方可最大化的利用编程语言中 Exception 的这一特性。
感谢您的阅读,如果您对 Exception 有其他的见解,欢迎在评论区留言讨论。