第六十九条:只针对异常的情况才使用异常【异常start】

某一天,如果你不走运的话,可能会碰到下面这样的代码:

// Horrible abuse of exceptions. Don't ever do this!
try {
  int i = 0;
  while(true) range[i++].climb();
} catch (ArrayIndexOutOfBoundsException e) { 
}

这段代码有什么作用呢?看起来根本不明显,这正是它没有真正被使用的原因(详见第67条)。事实证明,作为一个要对数组元素进行遍历的实现方式,它的构想是非常拙劣的。当这个循环企图访问数组边界之外的第一个元素时,用抛出(throw)、捕获(catch)、忽略ArrayIndexOutOfBoundsException的手段来达到终止无限循环的目的。假定它与数组循环的标准模式是等价的,对于任何一个Java程序员来说,下面的标准模式一看就会明白:

for (Mountain m : range) 
  m.climb();

那么,为什么有人会优先使用基于异常的循环,而不是用行之有效的模式呢?这是被误导了,他们企图利用Java的错误判断机制来提高性能,因为VM对每次数组访问都要检查越界情况,所以它们认为正常的循环终止测试被编译器隐藏了,但在for-each循环中仍然可见,这无疑是多余的,应该避免。这种想法有三个错误:
1、因为异常机制的设计初衷是用于不正确的情形,所以几乎没有JVM实现视图对它们进行优化,使它们与显式的测试一样快速。

2、把代码放在try-catch块中反而阻止了现代JVM实现本来可能要执行的某些特定优化。

3、对数组进行遍历的标准模式并不会导致冗余的检查。有些现代的JVM实现会将它们优化掉。

实际上,基于异常的模式比标准模式要慢得多。在我的机器上,对于一个有100个元素的数组,基于标准模式比异常模式快了2倍。

基于异常的循环模式不仅模糊了代码的意图,降低了它的性能,而且它还不能保证正常工作!如果出现了不相关的Bug,这个模式会悄悄地失效,从而掩盖了这个Bug,极大地增加了调式过程地复杂性。假设循环体中地计算过程调用了一个方法,这个方法执行了对某个不相关数组地越界访问。如果使用合理地循环模式,这个Bug会产生未被捕捉的异常,从而导致线程立即结束,产生完整的堆栈轨迹。如果使用了这个被误导的基于异常的循环模式,与这个Bug相关的异常将会被捕捉,并且被错误的解释为正常的循环终止条件

这个例子的教训很简单:顾名思义,异常应该只用于异常的情况下,它们永远不应该用于正常的控制流。一般的,应该优先使用标准的、容易理解的模式,而不是那些声称可以提供更好性能的、弄巧成拙的方法。即使真的能够改进性能,面对平台实现的不断改进,这种模式的性能也不可能一直保持。然而,由于这种过度聪明的模式带来了微妙的Bug,以及维护的痛苦却依然存在。

这条原则对于API设计也有启发。设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常如果类具有状态相关的方法,即只有在特定的不可预知的条件下才可以被调用的方法,这个类往往也应该有个单独的状态测试方法,即指定是否可以调用这个状态相关的方法。例如,Iterator接口有一个状态相关的next方法,及相应的状态测试方法hasNext。这使得利用传统的for循环(以及for-each循环,在内部使用了hasNext方法)对集合进行迭代的标准模式成为可能:

for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) { 
  Foo foo = i.next();
  ...
}

如果Iterator缺少hasNext方法,客户将被迫这样做:

// Do not use this hideous code for iteration over a collection!
try {
  Iterator<Foo> i = collection.iterator(); 
  while(true) {
    Foo foo = i.next();
    ... 
  }
} catch (NoSuchElementException e) { }

这应该非常类似于本条目刚开始时对数组进行迭代的例子。除了代码烦琐且令人误解之外,这个基于异常的模式可能执行起来也比标准模式更差,并且还可能掩盖系统中其他不相关部分中的Bug。

另一种提供单独的状态测试方法的做法是,如果"状态相关的"方法无法执行想要的计算,就让它返回一个零长度的optional值(详见第55条),或者返回一个可识别的值,比如null。

对于"状态测试方法"和"optional返回值或可识别的返回值"这两种做法,有些指导原则可以帮助你在两者之中做出选择。如果对象将在缺少外部同步的情况下并发访问,或者可被外界改变状态,就必须使用optional返回值或者可识别的返回值。因为在调用状态测试方法和调用对应的状态相关方法的时间间隔之中,对象的状态有可能会发生变化。如果单独的“状态测试”方法必须重复“状态相关”方法的工作,从性能的角度考虑,就应该使用可被识别的返回值。如果所有其他方面都是等同的,那么“状态测试”方法则略优先于可被识别的返回值。它提供了稍微更好的可读性,对于使用不当的情形可能更加易于检测和改正:如果忘了去调用状态测试方法,状态相关的方法就会抛出异常,使者这个Bug变得很明显;如果忘了去检查可识别的返回值,这个Bug就很难被发现。optional返回值不会有这方面的问题。

总而言之,异常是为了在异常情况下使用而设计的。不要将它们用于普通的控制流,也不要编写迫使它们这么做的API。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,718评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,683评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,207评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,755评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,862评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,050评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,136评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,882评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,330评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,651评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,789评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,477评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,135评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,864评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,099评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,598评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,697评论 2 351

推荐阅读更多精彩内容