New Java Feature - Exception
概述
- Java 异常的使用和处理,是滥用最严重,诟病最多,也是最难平衡的一个难题
- Java 语言支持三种异常的状况
- 非正常异常(Error)、运行时异常(Runtime Exception)、检查型异常(Checked Exception)
- 异常,除非特别声明,一般指的是 Checked Exception 和 Checked Exception
- 异常状况的处理会让代码的效率变低 - 不应该使用异常机制来处理正常情况
- 理想情况下,在执行代码时没有任何异常发生,否则业务执行的效率会大打折扣
- 几乎无法完成,不管是 JDK 核心类库还是业务代码,都存在大量的异常处理代码
- 软件都是由很多类库集成的,大部分类库,都只是从自身的角度去考虑问题,使用异常来处理问题
- 很难期望业务执行下来没有任何异常发生
- 抛出异常影响了代码的运行效率,而实际业务又没有办法完全不抛出异常
- 新的编程语言(Go),彻底抛弃类似于 Java 这样的异常机制,重新拥抱 C 语言的错误方式
性能
没有抛出异常的用例,能够支持的吞吐量要比抛出异常的用例大 1000 倍
案例
- 在设计算法公开接口时,算法的敏捷性是必须要要考虑的问题
- 算法总是会演进,旧的算法会过时,新的算法会出现
- 一个应用程序,应该能够很方便地升级它的算法,自动地淘汰旧算法,采纳新算法,而不需要太大的改动
- 因此,算法的公开接口经常使用通用的参数和结构
获取一个单向散列函数实例时,通过工厂模式来构造出单向散列函数的实例
1 | public abstract sealed class Digest { |
使用 of 方法,需要处理该 Checked Exception
1 | try { |
错误码
Go
既能返回值,又能返回错误码 - Go
1 | public record Coded<T>(T returned, int errorCode) { |
1 | public static Coded<Digest> of(String algorithm) throws NoSuchAlgorithmException { |
1 | Coded<Digest> coded = Digest.of("SHA-256"); |
基准测试 - 几乎没有差别
1 | Benchmark Mode Cnt Score Error Units |
缺陷
在性能优化的同时,放弃了代码的可读性和可维护性
需要更多的代码
- 使用异常处理的代码,可以在一个 try-catch 语句块里包含多个方法的调用
- 每个方法的调用都可以抛出异常
- 由于异常的分层设计,所有的异常都是 Exception 的子类
- 可以一次性处理多个方法抛出的异常
1 | try { |
- 使用了错误码的方式,每一个方法调用都要检查返回的错误码
- 一般情况下,同样的逻辑和接口结构,使用错误码的方式需要编写更多的代码
对于简单的逻辑和语句,可以使用逻辑运算符合并多个语句 - 紧凑但牺牲了代码的可读性
1 | if (doSomething() != 0 && |
对于复杂的逻辑和语句,需要一个独立的代码块来处理错误码 - 结构重复的代码会增加
1 | if (doSomething() != 0) { |
丢弃调试信息
性能 vs 可维护性
- 最大的代价 - 可维护性大幅度降低
- 使用异常的代码,可以通过异常的调用堆栈,清楚地看到代码的执行轨迹,快速找到出问题的代码
- 使用错误码之后,就不再生成调用堆栈了 - 性能提高
- 快速找到代码的问题,是一个编程语言的竞争力
- 如果回到错误码的处理方式,需要提供快速排查问题的替代方案
- 更详尽的日志 + JFR
易碎的数据结构
- 一个新机制的设计,必须要简单和皮实 - 不容易犯错
- 生成一个 Coded 实例,需遵循以下规则,违反任意规则,都可能产生不可预测的错误
- 错误码的数值必须一致,0 代表没有错误,其它值表示出错了
- 不能同时设置返回值和错误码
- 使用错误码,同样需要遵循规则
- 必须首先检查错误码,然后才能使用返回值
- 需要依赖编码人员的自觉发现,编译器本身不会帮忙检查
1 | public static Coded<Digest> of(String algorithm) { |
1 | Coded<Digest> coded = Digest.of("SHA-256"); |
需要自觉遵循的规则越多,犯错的概率越大
改进方案 - 共用错误码
同时考虑生成错误码和使用错误码两端的需求
1 | public sealed interface Returned<T> { |
- 封闭类的许可子类是可以穷举的
- 把 Returned 的许可子类(ReturnValue 和 ErrorCode)定义成档案类,分别表示返回值和错误代码
要么是返回值(ReturnValue),要么是错误码(ErrorCode)
1 | public static Returned<Digest> of(String algorithm) { |
使用错误码 - switch 匹配 - 必须使用 ReturnValue 或者 ErrorCode - 穷举
1 | public static void main(String[] args) { |
依然存在的缺陷 - 本身没有携带调试信息
小结
错误码 - 封闭类 + 档案类
- 不能携带调试信息
- 提高了错误处理的性能
- 增加了错误排查的困难
- 降低了代码的可维护性
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.