方式 1 和方式 2 的却别到底在哪里?

2019-11-16 11:13:45 +08:00
 Simle100

在开发中对于 a/b,这样的表达式,我们要做 b 的校验。但有两种方式: 方式 1: if ( b == 0 ) { // 提示除数不能为 0 } 方式 2: try{ a / b; } catch (ArithmeticException e){ // 提示除数不能为 0 } 这两种方式的差别是什么?是不是所有的处理异常的代码都被方式 1 取代?如果是这样,那么异常机制存在的理由是什么? 请大佬赐教。

6215 次点击
所在节点    Java
47 条回复
geelaw
2019-11-16 14:52:44 +08:00
区别在于第二种写法是错误的,因为 a/b 不是赋值、构造、方法调用、自增自减,所以无法构成 Java 的表达式语句。

异常是错误码的替代,不是预判的替代。
wysnylc
2019-11-16 15:03:37 +08:00
@felixlong #19 "3。try...catch 和 if...else 的性能开销在同一数量级" 这句话看不懂吗????
12tall
2019-11-16 15:07:28 +08:00
程序在处理 try {} 语句块时效率跟 if 几乎没差别;
但是在处理 catch{} 语句块的时候,慢了不止一个数量级

这是我在.NET 里面的经验,不知道 Java 有没有用
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 )
felixlong
2019-11-16 15:39:39 +08:00
@wysnylc 谁告诉你你的这个例子在一个数量级的。你有测过吗?要真纠结性能你这个例子里第二种要比第一种慢 100 倍以上。
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
```
ClericPy
2019-11-16 15:59:49 +08:00
这俩风格 google 搜 LBYL EAFP java 讲的很明白了, 包括什么场景使用哪种, 以及性能差距在哪里
为了避免又被你以为抬杠, 就说这么点吧
wysnylc
2019-11-16 16:01:04 +08:00
@crclz #24 赞同,业务异常和程序异常分开处理是对的
@felixlong #25
@guyeu #26 求求你们在 9102 年不要坚持这种愚蠢的想法了
wysnylc
2019-11-16 16:02:26 +08:00
@crclz #24 文章发一下谢谢,我放日经里去,下次不废口舌直接甩脸上
sunznx
2019-11-16 16:13:49 +08:00
@guyeu 感觉有点 zz,你没事抛几百万次异常?????
就算是性能差 1000 倍,那也只差几毫秒

for 快还是 while 快。MaDe。。。
wysnylc
2019-11-16 16:18:39 +08:00
@sunznx #30 就让他们活在"try-catch 性能差老师说过不要用"的世界吧,同样的还有"不要用 in 因为不会走索引"
guyeu
2019-11-16 16:26:46 +08:00
@wysnylc #28
@sunznx #30
爬了一下楼,二位想必是知道捕捉异常比条件判断慢的,的确是慢在了收集堆栈信息上,或许可以通过重写 Exception 父类的内容来避免,但是更合理的当然是使用条件判断而不是处理异常。
这个慢是数量级的慢,“只差几毫秒”?无数计算机科学家绞尽脑汁,无数材料化学家物理学家费尽心血,让现在的 CPU 可以在毫秒级里执行几百万次这样的逻辑,不是给你这种睿智这么浪费的。

异常就是异常,它代表程序的错误,而不是逻辑的一部分,不要用异常来实现逻辑。
wysnylc
2019-11-16 16:35:11 +08:00
@guyeu #32
你想想看,假如没有 try catch,你每调用一次函数,都需要去判断执行结果,判断方式自然是 if else。
当程序中这些会出错误的函数少还好,但是假设你一段代码中有大量的程序要做这做判断,而且一般都是相关的代码放在一起的。这就意味着后面执行的逻辑会依赖你前面语句的执行情况,也就意味着你每调用一个可能会出现错误的函数的时候,都要判断是否成功,然后再继续执行后面的语句。导致你的这段代码中充斥着大量的 if else。
更极端一点,假设你的这段充满了 if else 判断的代码封装在某个函数里面,然后外层又有函数调用你这段函数,是否意味着外面这个函数也要去判断异常情况?你的错误可能会使用某个整数来作为错误代码,来表示不同的错误情况,可能会大大影响程序的可读性。而且每一层代码的错误处理都要和你的逻辑代码混在一起,写到最后你自己都会觉得恶心。

异常机制( try catch )就是用来解决这个问题的。
异常机制将所有的程序异常的情况和正常执行的代码分离开来,并提供统一的代码去处理不同的异常,而且针对不同类型的异常情况定义了不同的异常类,用于表示不同的异常情况,增加代码可读性。java 还提供了受检异常和非受检异常,受检异常会强制你去写 try catch 去处理异常情况,否则可能导致编译不通过,这对代码的健壮性很有帮助,避免人为的遗漏异常处理。
wly19960911
2019-11-16 17:55:41 +08:00
@crclz 跟我想法一样。不过 dalao 还是更简练深刻,我讲的废话多…大佬有兴趣看我回复记录就能看见了。


@wysnylc 难怪开了一贴,原来在这里讨论…哈哈哈
wysnylc
2019-11-16 18:14:07 +08:00
@wly19960911 #34 看了你的我才发现 24 楼貌似是不推荐 try-catch 的,而是用所谓的 result 封装
带来的后果就是膨胀和无穷无尽的 if else
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.


不过回到题中的问题, 我觉得是一个规范(习惯)的问题. 这种问题可以讨论, 但是我觉得很难获得统一的结论.
iEverX
2019-11-16 19:01:24 +08:00
@wysnylc #33
从争论来看,并没有看到 @guyeu 说不用异常,而是有选择的使用异常。这里是 a / b,直接 try catch 可以。假如说之后业务有修改,要求 b > 0,那么直接 try catch 不还是要改 if (b <= 0) throw 吗。在这里,try catch 相对 if,在可读性、可维护性、性能上都没有优势。
mmixxia
2019-11-16 19:46:52 +08:00
精彩的讨论
MiffyLiye
2019-11-16 19:49:18 +08:00
API 名称和文档与具体行为匹配就好,不要给调用方错误的预期,剩下的如何处理就是调用方的责任了
此外不要把异常用成正常业务的流程控制手段

如果调用方的预期是总能得到正确的结果,则在无法满足外部期望的时候,应该 throw,尽早暴露问题并修复
如果调用方的预期是有些情况无法计算,则调用方应该用 Tester-Doer Pattern,或者调用返回 Option.Some / Option.None 的 API,无法计算作为正常业务场景,由调用方用 if else 去控制流程

至于性能,只要不是运行时频繁抛出来,几乎不可能成为性能瓶颈
如果你不是天才,就老老实实上 profiler 找瓶颈有针对性地去调,先学会走,再去学跑
crclz
2019-11-16 23:16:36 +08:00
@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 )

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/620135

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX