什么情况下可以使用异常处理来处理业务逻辑?

33

我认为在Java中(以及可能是任何具有异常处理的语言),普遍认为应该尽量避免使用异常处理来实际处理业务逻辑。通常,如果预计某种情况应该发生,那么应该直接检查并更直接地处理它,而不是依靠异常处理来为您进行检查。例如,以下示例不被认为是良好的做法:

try{
  _map.put(myKey, myValue);
} catch(NullPointerException e){
  _map = new HashMap<String, String>();
}

相反,惰性初始化应该更像这样实现:

if(_map == null){
  _map = new HashMap<String, String>();
}
_map.put(myKey, myValue);

当然,可能会有更复杂的逻辑,而不仅仅是处理延迟初始化。因此,考虑到这种类型的事情通常是不被赞同的...在何时、如果有时,依赖于异常发生以进行某些业务逻辑是一个好主意?可以说,任何时候使用这种方法的情况实际上都凸显了所使用的 API 的弱点吗?


3
这个“通用”的规则有例外。在实现异常机制非常不好的语言中(比如Java),除了普通的错误处理之外,使用异常并不是一个好主意。但在像OCaml这样的语言中,完全可以使用异常来控制流程,因为它们非常廉价且高效。 - SK-logic
4
这让我想起了Python中的迭代器。它们依靠StopException来停止迭代。对于这个答案的评论提到了Python的哲学:“宁愿请求原谅,也不要先问过许可”。 - Adam Paynter
1
听起来好像哲学更依赖于语言,这比我意识到的更有趣... - Michael McGowan
5个回答

35
无法避免但可预见的异常情况下,比如依赖于外部 API 来解析数据,该 API 提供了解析方法但没有提供判断给定输入是否能被解析的函数(或者是是否解析成功取决于您无法控制的因素,但 API 并未提供适当的功能调用),当输入无法被解析时,解析方法会抛出异常。如果 API 设计良好,这种情况应该在“几乎不可能”到“从不”的范围内。
我完全看不出使用异常处理作为代码正常流程控制的理由。它很昂贵,在阅读上很困难(只需看看您自己的第一个示例;我意识到它可能写得非常快,但当 _map 没有被初始化时,您最终得到的是一个空映射,丢弃了您正在尝试添加的条目),并且它使代码中充斥着大量无用的 try-catch 块,这些块很可能会隐藏真正的问题。再以您自己的例子为例,如果对 _map.add() 的调用由于某些原因而抛出 NullPointerException(与 _mapnull 不同的其他原因),会发生什么?突然间,您正在默默地重新创建一个空映射而不是向其中添加条目。我相信我无需真正说明这可能会因意外状态导致代码中完全不相关的任何数量的错误...
编辑:仅为明确起见,上述答案是针对 Java 写的。其他语言的异常实现成本可能有所不同,但其他观点仍然适用。

如果能指出我在实际示例中的错误(该示例是匆忙组合而成的,没有经过多少思考),并解释为什么这种做法不好,那就再加1分吧。 - Michael McGowan
是的,我想这可能是匆忙拼凑而成的,但这仍然是一个重要的考虑因素。你的例子只是强调了这一点。 - user
3
Python是一个有趣的例子,对于这个问题持有相反的看法。请参见此处的讨论:stackoverflowWikipediapython.net - eggsyntax
除此之外,(1)我的答案是在Java的上下文中编写的,原始问题的标签表明这是OP的主要关注点;(2)没有在Python中工作过,大多数观点仍然适用。例如,高风险的错误,如OP示例所示的那些错误。 - user

12

在C++中,抛出异常是一个比较昂贵的操作,在Java中则更加昂贵。仅从效率的角度考虑,避免显式检查并接受异常是毫无意义的。我想你可能能够在一些检查是否会抛出异常非常复杂或几乎不可能的罕见情况下进行辩解,但除此之外,我的答案是“几乎从不”。


更正:异常的实例化是昂贵的,但抛出异常并不是那么昂贵。而且,它也不是“极其”昂贵。 - axtavt
2
相对于空值检查,我会认为fillInStackTrace()非常昂贵。抛出异常也不便宜;虽然它不像C++那样涉及到调用析构函数,但却需要在每个堆栈层次中深入查找内部数据结构以找到适当的处理程序。 - Ernest Friedman-Hill

0

这实际上是一个讨论性问题,答案取决于您的系统设计。

我的直觉总是一概而论地不使用异常,但我见过几个系统使用异常来实现业务错误。我个人觉得这很恶心,但我真的理解在你知道它失败后立即中断业务流程、处理失败(例如回滚你的工作单元)并将错误返回给调用者,可能还会添加一些信息的优点。

可能的一个缺点是,在多个不同的类中进行错误处理非常简单,因此从代码中推断出当进程失败时发生的事情的定义非常困难。

无论如何,这里没有单一的答案,您需要权衡两种方法,并有时将它们结合起来。

关于您的示例,我不认为在“良好”(设计良好)的情况下使用异常来控制流程有任何优势。


-1

异常是对象的原因。Java语言的设计者将所有Throwable分为两种主要类型:已检查和未检查,也有其原因。

依赖于异常发生来触发某些业务逻辑是一个好主意吗?

是的。绝对是的。您应该阅读《Effective Java, Second Edition》的第9章。所有内容都在那里。清晰明了,等待着您的到来。


1
一个普通的已检查异常和我所描述的有所不同。我认为,已检查异常是指出现了意外情况,但存在合法的恢复方式。而我所描述的情况更多是当你实际上预料到“意外”会发生时。 - Michael McGowan
@Michael McGowan 有时候异常是实现某些功能的唯一途径。想想多线程和阻塞调用。 - user381105
1
本网站的目的是提供答案,而不是将某人重定向到另一个来源(即使该来源很好)。这与说RTFM没有什么区别。-1。 - Mark Peters
@Mark Peters做一些已经完成且以更好的方式完成的事情有什么意义呢? - user381105
2
首先,并不是每个人都有那本书。其次,这个网站的目标是成为一个信息的存储库。你没有回答问题,只是告诉他们去别处寻找答案。那不是一个答案,但它可以作为一个有效的评论。 - Mark Peters

-3

如果您正在处理实际上是业务逻辑的错误条件,那么使用异常是可以的。例如:

try{
   // register new user
   if(getUser(name) != null)
      throw new MyAppException("Such user already exists");
   //other registration steps......
}catch(MyAppException ex){
   sendMessage(ex.getMessage());
}

4
为什么不直接在 if 块内调用 sendMessage(),从而避免出现异常呢?如果需要的话,可以添加 breakcontinuereturn 或其他合适的流程控制语句。这样做也能使情况更加清晰明了。 - user
但是你可以争辩说,调用这个函数的人应该自己进行测试。使用检查和异常来保护你的不变量是好的,但这并不意味着你的库的客户端不应该做同样的事情来处理异常情况。 - Mark Peters
@Michael:我同意,但更现实的例子是异常被抛回给需要知道它发生的调用者。 - Mark Peters
2
非常抱歉,这样做一点也不好。在同一个方法中抛出和捕获异常是非常糟糕的风格,而且效率也很低。 - Ernest Friedman-Hill
@Mark:这属于我之前回答中“可以预见但无法避免”的范畴。你可以预见到在用户填写注册表单时已经创建了另一个同名的用户账户,但是在你自己实际创建用户账户之前,你无法避免这种可能性。因此,抛出异常是适当的,但并不能消除优雅地检查不变量的需要。然而,我也同意@Ernest的观点,即在同一方法中抛出和捕获异常都不是好的做法。 - user
尝试注册新账户的代码不应该关心如何向坐在电脑前的用户传达失败信息。要么抛出异常并让调用代码处理,要么让调用代码传入一个ProblemHandler对象(其中包含HandleProblem方法,可以弹出对话框或执行其他操作)。如果一个操作要么完全成功要么完全失败,最好抛出异常;而在可能部分成功的情况下(例如给定一组要添加的账户),最好允许逐个处理错误。 - supercat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接