在开发中对于 a/b,这样的表达式,我们要做 b 的校验。但有两种方式: 方式 1: if ( b == 0 ) { // 提示除数不能为 0 } 方式 2: try{ a / b; } catch (ArithmeticException e){ // 提示除数不能为 0 } 这两种方式的差别是什么?是不是所有的处理异常的代码都被方式 1 取代?如果是这样,那么异常机制存在的理由是什么? 请大佬赐教。
1
712e1959 2019-11-16 11:20:14 +08:00 via Android
省劲儿。
|
2
Simle100 OP @projectceiling 我提的问题,跟这个差不多:一个方法的参数,C 里面好像是一堆的 if else 判断参数是否合法,不合法就返回一个没有实际意义的值
但在 java 里面有异常机制,当参数不合法的时候,究竟是 if else 一样判断后返回一个值 还是直接来个 Exception ? 如果是 if else 的话,有什么好处 ? 如果是 exception 的话,又有什么好处 ? 或者是根据不同情况来定 ? |
3
Simle100 OP @projectceiling 处理异常的代码不是更复杂吗?这股劲省在哪里了啊?
|
4
vikeria 2019-11-16 11:38:04 +08:00
异常机制蛮消耗性能的
|
5
hdbzsgm 2019-11-16 11:41:42 +08:00 1
EAFP 与 LBYL 是两种编程风格 用哪个都行
|
6
leeg810312 2019-11-16 11:44:01 +08:00 via Android
try catch 在执行时比 if 的资源消耗大很多,能够用 if 解决不要用 try
|
7
Simle100 OP |
8
wysnylc 2019-11-16 11:51:42 +08:00 1
9120 年了还有人在说不推荐用异常异常性能差呢哈哈哈哈,要是异常性能这么差为啥 jvm 不优化或者不去掉呢?
我推荐使用 2,因为可以准确捕获到除数不能为 0 的异常可以针对的处理,而且并不是每次都为 0 那么每次都多个 if 判断谁的性能差? jvm 存在的意义就是给你抹除掉在 C 中因为考虑性能而需要在代码中做的种种优化,让开发者更注重于代码的健壮性和语义而不是在开发阶段考虑所谓的"性能",诚然是需要考虑性能但是是需要在并发环境下考虑而不是在这微不足道的 if else 上 |
9
ysoserious 2019-11-16 11:53:10 +08:00
方式 1 要考虑到各种可能的异常情况(比如你举的例子中就没有判断 null==b)
而方式 2 可以直接把程序异常抛出去让人看, 缺点是不一定人人都看得懂错误信息, 如果你针对错误情况给出人人都能看得懂的提示的话, 又回到方式 1 去考虑各种异常情况了. |
10
wysnylc 2019-11-16 11:54:43 +08:00
很多语言都存在异常处理机制,都是推荐使用 EAFP 方式,因为这样开发者可以注重于开发正常的逻辑而不是时时刻刻考虑 if else 判断,LBYL 方式会导致正常的逻辑穿插各种检查代码反而导致难以读懂
这里有一篇分析你可以看看 http://ju.outofmemory.cn/entry/367391 |
11
murmur 2019-11-16 11:55:30 +08:00
@wysnylc 这个东西我持保留意见,我见过那种代码,除了 mybatis 提供的转义之外,什么参数都不校验,数据错了插不进去是异常,流程或者权限错了工作流报异常。。小业务是没问题,但是总看着觉得不舒服
|
12
wysnylc 2019-11-16 12:05:07 +08:00
@murmur #11 那么在方法中将返回值定义成各种对象里面包含对应的状态码和结果就好了吗?试想一个场景,用户购买物品失败如果不使用异常控制则需要在 serverice 方法的返回值中定义 code message result 来告知上层方法本次失败的原因,这种设计是不是比异常控制更恶心?
而用异常控制就很简单了,不会因为需要兼容失败情况而修改方法返回值,只需要捕获不同的异常和异常自定义信息就可以 孰优孰劣,一用便知 |
13
taogen 2019-11-16 12:05:52 +08:00 via Android
知道异常输入值是什么可以用 if 判断,不知道用 try catch。如,SQL 异常,数据库连接异常等。
if else 和 try catch 目的都是对正常和异常情况分开处理。 |
14
chendy 2019-11-16 12:11:31 +08:00
明显用 1 不用 2,1 比 2 清晰太多了…只因为清晰,无关性能(性能其实真的差,但是和 io 比还是微不足道
从 crud 的角度看,异常更多时候是用来抛的,手动 catch 的情况并不多 遇到数据错误,逻辑不对,直接抛个异常中断业务,最外层全局捕获转换报错信息发出去 |
15
wysnylc 2019-11-16 12:12:16 +08:00
@leeg810312 #6
@vikeria #4 关于异常性能的问题麻烦看这里:https://blog.csdn.net/wenbingoon/article/details/8240862 直接拉到最底下有这么几句话 1。Exception 的性能是差,原因在于 Throwable fillInStackTrace()方法() ----- public synchronized native Throwable fillInStackTrace(); 2. 可以通过改写业务异常基类的方法,提升性能 3。try...catch 和 if...else 的性能开销在同一数量级 |
16
hdbzsgm 2019-11-16 12:14:02 +08:00
@Simle100 #7 因为角度不同 callee 跟 caller 的处理边界或者处理异常的思路是有区分的 但是一个原则就是要给 caller 反馈到足够的错误信息 你写 if/else 去反馈 框架用 try/catch 去反馈 都可以
|
17
optional 2019-11-16 12:24:09 +08:00 via iPhone
JAVA 又没有 some(x)这种 enum
|
18
potcode99 2019-11-16 13:41:25 +08:00
防御式编程和 Java 语言特性的区别,换个不支持异常捕获的语言就只能用 if else,if 的写法更通用一些吧
|
19
felixlong 2019-11-16 13:41:54 +08:00 via Android
@wysnylc jvm 再优化能优化到比 if 还好?不要想当然的以为 jvm 可以解决一切问题。
|
20
vikeria 2019-11-16 14:08:24 +08:00
@wysnylc 异常处理的性能消耗主要还是在堆栈信息的跟踪上,打印有利于问题定位,在现在看确实微不足道,只是略提一下。正常编码规范一般不会推荐在异常处理中做业务逻辑,实际操作中,还是具体问题具体分析吧。每个人的理解和编码习惯不同,有利于代码健壮性和可维护性即可。
|
21
geelaw 2019-11-16 14:52:44 +08:00
区别在于第二种写法是错误的,因为 a/b 不是赋值、构造、方法调用、自增自减,所以无法构成 Java 的表达式语句。
异常是错误码的替代,不是预判的替代。 |
22
wysnylc 2019-11-16 15:03:37 +08:00
@felixlong #19 "3。try...catch 和 if...else 的性能开销在同一数量级" 这句话看不懂吗????
|
23
12tall 2019-11-16 15:07:28 +08:00
|
24
crclz 2019-11-16 15:38:27 +08:00
别信楼上大多数人的。团队的代码质量就是被他们败坏的。
[关于异常的收藏文章]( https://i.loli.net/2019/11/16/cMDWxm67A9bRYHI.jpg) 这些文章很有必要读。 不想看文章可以看结论: 分成写 library 和写业务代码。 写 library 时检查参数+抛出异常( go 另说)+几乎不捕获异常。 写业务的时候因为前端所接收到的返回内容是业务逻辑层决定的,所以对业务逻辑的封装成的 utils/helper-class/transactional-script 的大部分方法应当使用 XXXResult 作为返回对象。当然这个 XXXResult 怎么设计又是一门学问。不过别嫌烦。golang 都不嫌烦。 写业务的时候也要检查参数,抛出异常,例如字符串为 null 的时候,显然是代码哪里有问题。这都是要和调用者有一定约定(文档)的。 例外:数据库插入的唯一冲突。数据库插入冲突只能由数据库底层机制避免,而不是简单的参数检查。所以这里可以适当捕获异常。更好的做法是 upsert ( ON CONFLICT DO NOTHING RETURNING id ) |
25
felixlong 2019-11-16 15:39:39 +08:00 via Android
@wysnylc 谁告诉你你的这个例子在一个数量级的。你有测过吗?要真纠结性能你这个例子里第二种要比第一种慢 100 倍以上。
|
26
guyeu 2019-11-16 15:53:32 +08:00
@wysnylc #22
@felixlong #25 用异常来实现逻辑毫无疑问是错的,我写了一个小例子,异常比条件判断慢 100 倍以上: ```java public static void main(String[] args) { long cur = System.currentTimeMillis(); final int LOOP = 100_000; for (int i = 0; i < LOOP; i++) { try { int b = 0; int a = i / b; } catch (ArithmeticException ignored) { } } long cost = System.currentTimeMillis() - cur; System.out.println("cost1: " + cost); cur = System.currentTimeMillis(); for (int i = 0; i < LOOP; i++) { int b = 0; int a; if (b != 0) { a = i / b; } } cost = System.currentTimeMillis() - cur; System.out.println("cost2: " + cost); } ``` 输出如下: ``` cost1: 1199 cost2: 4 ``` |
27
ClericPy 2019-11-16 15:59:49 +08:00
这俩风格 google 搜 LBYL EAFP java 讲的很明白了, 包括什么场景使用哪种, 以及性能差距在哪里
为了避免又被你以为抬杠, 就说这么点吧 |
28
wysnylc 2019-11-16 16:01:04 +08:00
|
30
sunznx 2019-11-16 16:13:49 +08:00
|
31
wysnylc 2019-11-16 16:18:39 +08:00
@sunznx #30 就让他们活在"try-catch 性能差老师说过不要用"的世界吧,同样的还有"不要用 in 因为不会走索引"
|
32
guyeu 2019-11-16 16:26:46 +08:00
|
33
wysnylc 2019-11-16 16:35:11 +08:00
@guyeu #32
你想想看,假如没有 try catch,你每调用一次函数,都需要去判断执行结果,判断方式自然是 if else。 当程序中这些会出错误的函数少还好,但是假设你一段代码中有大量的程序要做这做判断,而且一般都是相关的代码放在一起的。这就意味着后面执行的逻辑会依赖你前面语句的执行情况,也就意味着你每调用一个可能会出现错误的函数的时候,都要判断是否成功,然后再继续执行后面的语句。导致你的这段代码中充斥着大量的 if else。 更极端一点,假设你的这段充满了 if else 判断的代码封装在某个函数里面,然后外层又有函数调用你这段函数,是否意味着外面这个函数也要去判断异常情况?你的错误可能会使用某个整数来作为错误代码,来表示不同的错误情况,可能会大大影响程序的可读性。而且每一层代码的错误处理都要和你的逻辑代码混在一起,写到最后你自己都会觉得恶心。 异常机制( try catch )就是用来解决这个问题的。 异常机制将所有的程序异常的情况和正常执行的代码分离开来,并提供统一的代码去处理不同的异常,而且针对不同类型的异常情况定义了不同的异常类,用于表示不同的异常情况,增加代码可读性。java 还提供了受检异常和非受检异常,受检异常会强制你去写 try catch 去处理异常情况,否则可能导致编译不通过,这对代码的健壮性很有帮助,避免人为的遗漏异常处理。 |
34
wly19960911 2019-11-16 17:55:41 +08:00 via Android
|
35
wysnylc 2019-11-16 18:14:07 +08:00
@wly19960911 #34 看了你的我才发现 24 楼貌似是不推荐 try-catch 的,而是用所谓的 result 封装
带来的后果就是膨胀和无穷无尽的 if else |
36
Raymon111111 2019-11-16 18:36:29 +08:00
抛错性能确实会差点
按照大佬的说法主要是限制了 jvm 优化代码的可能 来自 effective java Placing code inside a try-catch block inhibits certain optimizations that modern JVM implementations might otherwise perform. 不过回到题中的问题, 我觉得是一个规范(习惯)的问题. 这种问题可以讨论, 但是我觉得很难获得统一的结论. |
37
iEverX 2019-11-16 19:01:24 +08:00
|
38
mmixxia 2019-11-16 19:46:52 +08:00
精彩的讨论
|
39
MiffyLiye 2019-11-16 19:49:18 +08:00
API 名称和文档与具体行为匹配就好,不要给调用方错误的预期,剩下的如何处理就是调用方的责任了
此外不要把异常用成正常业务的流程控制手段 如果调用方的预期是总能得到正确的结果,则在无法满足外部期望的时候,应该 throw,尽早暴露问题并修复 如果调用方的预期是有些情况无法计算,则调用方应该用 Tester-Doer Pattern,或者调用返回 Option.Some / Option.None 的 API,无法计算作为正常业务场景,由调用方用 if else 去控制流程 至于性能,只要不是运行时频繁抛出来,几乎不可能成为性能瓶颈 如果你不是天才,就老老实实上 profiler 找瓶颈有针对性地去调,先学会走,再去学跑 |
40
crclz 2019-11-16 23:16:36 +08:00 2
@wysnylc 发出几个异常收藏的文章的链接。我顺便给大家概括一下。
##A https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/exception-throwing 这是微软的关于如何设计 library ( framework design guideline )的电子书的一个章节。注意看左边子目录里面共有 4 篇关于异常的文章(第一篇是简介)。 注意这是关于如何设计 library 的文章,其中大部分东西可以适用于业务上,例如 test-doer 模式、try-parse 模式;而某些条条框框不能套在业务代码上面,例如 "DO NOT use error codes" 。 当然,关于 library 使用异常还是 go 风格的东西(error codes),还需要深入研究。不过已知的是,java/c#的东西久经检验,不会差。 另外,如果心存疑问的话,可以去 GitHub 上面 clone 一下 CoreFx (.net core 标准库)的代码。我简单的看了少部分,发现: 1. 大部分 throw 代码,抛出的都是 ArgumentException (ArgumentException, ArgumentNullException, ArgumentOutOfRangeException),还有 InvalidOperationException。为什么会这样呢?答案很自然:因为出了问题,问题肯定在 caller 的传参上面,或者在 [当前对象的状态不合适] 上面。正如 C# Docs 所言,InvalidOperationException is used in cases when the failure to invoke a method is caused by reasons other than invalid arguments. 2. 这些异常的抛出都是这样抛出的 if(argument x not satisfy some condition) throw ArguementXXXXXException. 并且每个函数开头几乎都会检查所有参数。(这很自然。记住这样做也是标准做法。) 3. 很少能看见捕获异常的代码。所以不要动不动就捕获异常。总而言之,你的 library 和业务都应几乎不出现 catch。这些东西你都不用关心。想一想 catch 了也没啥用。 4. 小部分捕获异常的代码,几乎捕获的都是 InvalidCastException 之类的. InvalidCastException 是由于失败的转换类型抛出的。这都是在架构里面属于有点底层的东西,可能某些东西设计不当,我也没深究,平时应该不会碰到。 5. 我也看了 EntityFrameworkCore 的代码。发现,也不能说完全不用 catch 吧。EF 的 catch 还涉及到这样的东西:捕获 - 记录(log) - rethrow。如果你有这样做的需求,你可以这样做。 6. 捕获异常的操作还可用于:(节选自 docs of Exception.InnerException )“你可以创建一个新的异常来捕获更早的异常。 处理第二个异常的代码可以利用以前异常中的其他信息来更正确地处理错误。”。 我的评论:EFCore 也有少量这种代码,但是平时会很少有这种应用场景。设计 Library 的时候,如果你想要捕获异常后加点料,也可以加在 Exception.Data 属性。docs of Exception.Data: "to store and retrieve supplementary information relevant to the exception"。 7. 内层的不恰当设计(或者天生的缺陷),会影响外层的代码。这很好理解:如果业务 HelperClass 使用异常来返回错误代码,那么外层的代码也会被迫用这种愚蠢的方式写组织代码。第二,何谓天生的缺陷?我的理解是"互操作"的天生不足。你去调用一个其他语言的东西(例如 c 语言的),这些东西和契合的本来就不如当前语言好。 ## B 和 C 这两篇文章能加深对异常处理的理解。 http://www.informit.com/articles/article.aspx?p=433387 https://enterprisecraftsmanship.com/posts/error-handling-exception-or-result/ ## D https://martinfowler.com/articles/replaceThrowWithNotification.html 我直到刚才,才发现,这篇文章是 Martin Fowler 写的!!!!! 它讲了 Validations (验证输入)时应该以 Notification 的方式(类似于一个 Result Class,或许可以将业务的 result class 和这个结合起来)。主要场景大概是处理 http 接口传入的参数。( ModelValidation ) |
41
crclz 2019-11-16 23:54:15 +08:00
关于业务代码的错误处理,我的理解:
封装好的业务逻辑方法( HelperClass, Utils ),应当返回一个 Result。这个 Result 可以组织成这个结构: { ErrorCode String Data Dictionary<string, string> } ErrorCode:用于给前端传递约定好的错误代码,例如'NicknameAlreadyExist'。 Data:用于给前端传递进一步细化错误的数据。例如前端传过来一个请求,要转账 400 元。你和前端约定,如果不足 400 元,为了告诉用户更多信息,就将它的余额{money: 300}加在 data 里面。 还可以设置一些字段支持多个错误代码,但我觉得大部分时间没必要。 还可以设置一些字段来告诉 http 请求发起者更多信息,详见各大 api 规范,例如 https://github.com/Microsoft/api-guidelines/blob/master/Guidelines.md 。 还可以设置一些字段来储存调试信息,这些信息可能会帮助到你再看错误日志的时候快速 debug。 但是,这样做有几个需要注意到的要点。 这样设计 HelperMethod 实际上是 Try-Parse Pattern。 HelperMethod 可能会被直接拿去用,也可能会被 HelperMethod 嵌套使用。当直接用的时候,调用者相当于客户端。当被其他 HelperMethod 嵌套调用的时候,大部分情况是像 GoLang 一样返回。也还有其他情况,例如:在方法 A 中,我调用方法 B。方法 A 的代码已经对一些条件进行了数据库的查询、检查。这些检查,可能碰巧的涵盖了方法 B 的检查。所以,方法 A 此时认为,对 B 的调用是一定会返回 success 的。所以如果返回了一个 error,那么就应当直接抛出异常(或者在 error 里附带这一层的某些有助于调试的信息),Exception.Data 里面附带这个 error。这很自然,因为这个内层的 error 不该返回给前端,因为用户会对这个 error 里面的内容可能会不知所云。 异常抛出了,然后被你的 http 框架( spring/WebApi )捕获,然后被你的 logger 记录。你去看 log,里面你记录的调试信息就可能会帮助你还原问题,尽快 debug. --- 跳出异常的话题。HelperClass 虽然被广泛使用,但有很多弊端。 在性能上,可能重复查数据库很多遍,检查某个条件。 在开发效率上,会出现领域知识的割裂、分散;持久层逻辑的污染;健忘。DDD 是解决方案。 |
42
MorningBOBO 2019-11-17 14:46:55 +08:00
方式 2,耗损一些性能,换取可读性.
|
43
cyspy 2019-11-17 20:46:34 +08:00
除 0 是可以被防御的,而大部分 IOException 是无法防御的
|
44
vjnjc 2019-11-17 21:56:57 +08:00
强烈赞同#8 的看法。
使用 Java 就是要用 Java 的方式。理论上别人声明一个方法可能跑出 A 异常,那你就不该去 if 判断做冗余的事情。因为有些 runtime 的问题很复杂,你检查过了不代表 if 后面的情况还适用 |
47
guyeu 2019-11-18 14:54:30 +08:00
@wysnylc #33 同意你的大部分内容,但是绝对不同意应该用异常来实现逻辑。
比如 b/a 这个场景,不要傻乎乎得直接除+捕捉除 0 异常,应该检查传入参数, if (a == 0) throw new IllegalArguementsException("a cannot be zero."); 滥用异常来实现逻辑一方面大量的异常会造成效率损失,另一方面 try catch 块的确会侵入正常逻辑造成可读性变差。 另外,大多数开发者的能力并不足以驾驭实现这样的异常处理逻辑,所以,防御式编程,在发现问题之后抛出异常,但是不要制造问题。 |