08 | 抛出异常,是不是错误处理的第一选择?
阅读案例
- 深入了解
- 翻译
- 解释
- 总结
本文探讨了Java异常处理的滥用和性能问题,以及对代码执行效率的影响。通过基准测试展示了没有抛出异常的用例支持的吞吐量远高于抛出异常的用例,并从运营成本的角度说明了异常处理对资源消耗的影响。作者还讨论了异常处理在算法升级和公开接口设计中的应用,以及对性能的影响。文章最后提到了一些新的编程语言放弃异常机制的趋势,重新拥抱错误码方式。总体来看,本文通过案例和代码展示了Java异常处理的滥用和性能问题,引发了对异常处理机制的思考和讨论。文章还掴握了重回错误码的选择代价,包括代码的可读性和可维护性的降低,以及丢弃了调试信息和易碎的数据结构等缺陷。文章提出了使用错误码的方式需要更多的纪律,容易产生错误,需要改进的方案来减少这些额外的要求。文章还介绍了一种改进的设计,使用封闭类和档案类来分别表示返回值和错误码,提供了一个精简的方案。通过这种方式,文章呼吁对Java的错误处理进行更好的设计和探索,以提高代码的完善性。
《深入剖析 Java 新特性》,新⼈⾸单¥59
全部留言(10)
- 最新
- 精选
- 郑晔这篇文章不算是 Java 新特性的介绍,而是基于 Java 新版本特性进行编码方式的一种探索。 这种做法类似于 Rust 中提供的 Result。 https://doc.rust-lang.org/std/result/ 我在之前的项目中做过类似的探索,就像下面这样 public abstract class Result<T, U> { public abstract boolean isOk(); public abstract T getValue(); public abstract U getReason(); public static <T, U> Result<T, U> ok(final T value) { return new OkResult<>(value); } public static <T, U> Result<T, U> error(final U reason) { return new ErrorResult<>(reason); } ... } 经过探索,我发现,这种代码在局部很好用,但是在于框架(比如 Spring)结合时就比较麻烦,主要原因还是现有框架很多是通过抛异常的方式往下走,比如,Spring 的事务回滚,与之结合就比较难看。 采用新版本特性之后,用 sealed 实现效果比我的版本效果要好,更加严格了,用模式匹配,也比我的版本到处去判断 isOk 要清晰一些。在实际的应用中,我给 Result 类提供了 andThen 之类的方法(参考 Optional 的 map、flatMap),让程序员可以写更多连续的声明,而不必连续的判断 isOk。 但范老师这个版本也有一些不足,首先是使用这个实现要知道 ReturnValue 和 ErrorCode,这是暴露了一些底层实现细节的做法。此外,还要有从 ReturnValue 中取值的操作,也就是 rv.returnValue(),相比于 Rust 的 Result 直接进行模式匹配去取值,也是要多了解一些细节。 总的来说,这种编码尝试是很好的,只是为啥不尝试把它加入到 JDK 中呢?如果在 JDK 中有这种类,一方面可以更多地影响现在框架的处理方式,一方面可以促进模式匹配实现方式的改进,一举多得。
作者回复: 没想到郑老师来看这篇文章, 并且留下了精彩的评论。 谢谢郑老师! 这个想法还有一些待完善的地方, 比如编译器怎么检验错误码; 比如模式匹配能不能改进,这样用户可以少一步调用。 看看更多时间的琢磨,有没有办法让它变得更简单、皮实。
2021-12-03235 - Jxin1.这个用法眼前一亮。对于 rpc调用 返回 Result格式 的风格,采用这个写法不论调用方还是服务方,代码看起来都更干净整洁,可以有效提高可读性,提高知识传承的效率。 2.借助日志框架,你是能精细化到哪个类哪行代码打了这个日志。只要再把当前函数的关键入参追加在日志里,其实就足以发现并处理大部分问题了。需要堆栈的场景其实是为了还原真实调用的链路。假设入参组装有问题,就可以顺着这个链路往上翻找到可能出现组装问题的代码。但我觉得健壮的代码,问题应该在组装出错的地方就抛出来,而不是下沉到下层调用才发现。可惜人多了,这种情况其实避免不了,所以两害取其轻我还是觉得得打印堆栈。可维护性高于性能,另外真实业务有很多耗时大的io请求,所以异常堆栈的性能影响其实没有文中测试这样大的差距。 3.这个写法,无法起到异常直接阻断链路的效果。如果有多层函数嵌套,需要每一层都对函数回参做处理才能实现下层函数阻断上层函数后续逻辑的效果。这也要多写很多代码,既成本不菲,还污染了业务代码,降低可读性。 4.大部分的业务运行期异常其实是判 null 。这是一个老生常谈的问题。不用 nullobj 采用 Optional 代码可以变得更干净些。 使用 Optional 不校验 null,最难受的点在于多个map转换,你无法知道在哪个map返回了null,导致最终为null。如果写个 识别上述的档案类的 "Optional" ,出现异常返回缺省打印日志且中断当前链路,感觉可以让这个写法有更好的应用。如果每个函数都要写回参处理是绝对推行不开的。 5.不论语法如何升级,异常这个都很难被完全拿掉。因为异常机制使用太广太深了,业务代码/框架/语言内库都大量使用,作为依赖方你就必须感知并处理这些异常。所以如果为了提升性能,从异常堆栈打印本身去优化会不会也是一条思路?一但有效已有项目都能受惠,也算为激励大家升级jdk添砖加瓦。
作者回复: 留言精彩啊! 2、也有很多场景,异常的性能开销无法忍受。 3、是的,错误码的缺陷之一。我期望的是下一讲里提到的(就一句话),Java异常处理自身的改进。目前能够看到的替代办法,都有或多或少的问题。 4、这个想法有意思! 5、参见3.
2021-12-018 - aoe性能优先的场景:多主动判断,少抛异常,只能辛苦程序员了 对性能没追求的场景:尽情抛异常,潇洒写代码,剩下的交给 JVM
作者回复: 哈哈,要是又潇洒又有性能,就理想了。
2021-12-022 - 小飞同学小疑问: Digest.of返回错误码的同时,新增一个属性把抛出异常的位置返回是否合适Thread.currentThread().getStackTrace()[1]。 思考题:可以用类型匹配,JDK16已正式发布。 if(rt instanceof ReturnValue rv && rv.returnValue instanceof Digest d){ d.digest("Hello, world!".getBytes()); }else if(rt instanceof ErrorCode e){ System.out.println("Failed to get instance of SHA-256"+e.errorCode); }
作者回复: 返回异常的话,依然需要生成异常,不一定有多少性能的改进。
2021-12-012 - Geek_045c20异常触发性能问题 请问是catch住就会产生性能问题 还是catch里面进行日志打印本身产生会性能问题
作者回复: 只要有异常生产,就会产生性能问题。这取决于异常的机制和内部实现。
2022-10-08归属地:美国 - fatme要更好地把正常结果和错误结果封装起来,难点在于我们没有办法用一个统一的行为,去抽象对这两种结果的不同处理。这样,调用者就不得不判断返回的结果是属于哪种情况,进而采取不同的动作。要么根据返回值的内容进行判断,比如 errorCode 字段或者相应方法;要么根据返回值的类型进行判断,如 ReturnValue 和 ErrorCode。或者我们可以换个思路,既然无法把两种结果的处理行为统一起来。那么,我们是否能够把这种不可避免的判断逻辑,交给返回结果自己去处理,从而调用者毋需关心,这样对于调用者的代码来说,就能够统一为一种方式了。利用 lambda,在 jdk 8 我们可以这样写: public enum ErrorCode { UnknownAlgorithm, } interface Returned<T> { void processResult(Consumer<T> okResultProcessor, BiConsumer<ErrorCode, String> errorResultProcessor); } public class OkResult<T> implements Returned<T> { private final T result; OkResult(T result) { this.result = result; } public void processResult(Consumer<T> okResultProcessor, BiConsumer<ErrorCode, String> errorResultProcessor) { okResultProcessor.accept(result); } } public class ErrorResult<T> implements Returned<T> { private final ErrorCode errorCode; private final String reason; ErrorResult(ErrorCode errorCode, String reason) { this.errorCode = errorCode; this.reason = reason; } public void processResult(Consumer<T> okResultProcessor, BiConsumer<ErrorCode, String> errorResultProcessor) { errorResultProcessor.accept(errorCode, reason); } } // in Digest class public static Returned<Digest> of(String algorithm) { switch(algorithm) { case "SHA-256": return new OkResult<Digest>(new SHA256()); case "SHA-512": return new OkResult<Digest>(new SHA512()); default: return new ErrorResult<Digest>(ErrorCode.UnknownAlgorithm, "oops!"); } } // in main Consumer<Digest> okResultProcessor = result -> {System.out.println("Got Digest instance: " + result);}; BiConsumer<ErrorCode, String> errorResultProcessor = (errorCode, reason) -> {System.out.println(reason);}; Returned<Digest> rt = of("SHA-256"); rt.processResult(okResultProcessor, errorResultProcessor); rt = of("ooo"); rt.processResult(okResultProcessor, errorResultProcessor);
作者回复: 这也是一个思路。 不过回调函数式的设计,很快就会陷入回调地狱,代码很难看,很难懂。 所以, 除非是高级别的API, 我们在底层API里,很难使用回调函数式的设计。
2021-12-12 - ABChttps://github.com/XueleiFan/java-up/pull/15
作者回复: 在PR里留言了,要是代码的规范在留意些,就更好了!
2021-12-01 - ABC其实错误码应用很广泛,也很方便.比如微信支付,支付宝支付等应用给第三方的接口文档,基本全都是错误码模式,返回格式是json.一个错误码,一个错误消息,一个消息主体,如果没有错误就返回0,错误消息为空.只是在使用的时候各不相同而已. 我记得,在Java里面抛出异常,会一直从触发异常的地方调用到栈顶,这是其中一个原因,是这样吗,老师?
作者回复: 异常的触发是这样的,这也就是我们说的堆栈信息。
2021-12-012 - ifelse学习打卡2022-10-10归属地:浙江
- Jagger Chen点赞,特别棒的点在于编译器可以帮助检查错误。 《码出高效 Java 开发手册》上有这样一段描述: 推荐对外提供的开放接口使用错误码;公司内部跨应用远程服务调用优先考虑使用 Result 对象来封装错误码、错误描述信息;而应用内部则推荐直接抛出异常对象。2021-12-06