Java中采用“异常驱动开发”的性能成本是多少?

16

在Java中,创建、抛出和捕获异常是否会带来性能成本?

我计划将“异常驱动开发”应用于一个大型项目中。我想设计自己的异常并将它们包含在我的方法中,强制开发人员捕获并进行适当的处理。

例如,如果您有一个根据名称从数据库获取用户的方法。

public User getUser(String name);

然而,用户可能为空,很容易忘记在使用用户的公共方法之前检查这一点。

User user = getUser("adam");

int age = user.getAge();

如果未进行检查并返回空用户对象,会导致NullPointerException和崩溃。但是,如果在返回用户对象之前进行检查并抛出“UserIsNullException”异常:

那么就可以避免这种情况的发生。

public User getUser(String name) throws UserIsNullException;

我迫使实现者去思考和行动:

try {

    User user = getUser("adam");

    int age = user.getAge();

}catch( UserIsNullException e) {

}

这种设计模式使代码更加安全,可以避免意外崩溃和减少更多的 bug。假设网站每小时有数百名访问者,并且这种设计模式在很多地方都被使用。

这种设计方式会对性能产生什么影响?它的好处是否超过成本,还是只是简单的糟糕编码?

感谢任何帮助!

更新!为了明确起见,我的注意力不是包装 NullPointerException,正如我的示例所暗示的那样。目标是强制实现者编写try/catch,避免由于忘记处理一个:

user == null

而导致真正的崩溃。问题涉及比较这两个设计模型:

int age;

try {

User user = getUser("adam");

age = user.getAge();

}catch( UserIsNullException e) {

age = 0;

}

对比:

int age;

User user = getUser("adam");

if( user != null ) {
age = user.getAge();
} else {
age = 0;
}

6
空对象可能对你有所帮助。我前几天在阅读Martin Fowler的《重构》一书时,他描述了使用一个特定对象来代替返回null,这样就不必一直检查NULL了。如果你有这本书,可以翻到第260页的“引入空对象”章节。我不会在这里解释,因为书中已经讲得很清楚了。 - Tony
@Tony 我认为你的评论值得成为一个完整的答案 :-) - KLE
5
@corgrath:在catch部分,程序员会做什么?我认为他们大多数人最终会选择……什么都不做,忽略异常并继续执行。最终应用程序无论如何都会崩溃,但是会出现一个微妙、更难停止的异常。 - OscarRyz
一个快速的测试表明,在我的机器上,抛出和捕获异常比使用 if 语句慢了约 400 倍。 - Alex Feinman
如果你今天阅读了这个问题,最正确的做法是返回一个 Optional<User> 而不是一个 user/Exception。虽然不完美但很好。 - Haakon Løtveit
显示剩余2条评论
14个回答

12

抛出异常会有性能损失,但通常情况下这是可以接受的,因为异常处理代码只在异常情况下执行。如果你开始使用异常来控制流程,你就颠倒了通常情况,并将它们转换为预期行为。我强烈建议不要这样做。


8
"这将导致NullPointerException并崩溃。"
NullPointerException是一种异常,可以在catch块中捕获。因此,添加额外的异常不必要,除非它为代码增加了清晰度。在这种情况下,它并没有。事实上,你刚刚把一个未经检查的程序员错误异常转换成了已检查的异常。
为程序员的错误创建已检查的异常实际上意味着您的代码必须明确处理程序员引入错误的可能性,而他们并没有。"

10
因为程序员被迫在各处添加try-catch以捕获新的异常,所以会减少代码的清晰度。如果没有这种情况,程序员只需要编写解决问题的代码而不是检查自己是否出错,代码量就会很少。 - Will
1
@corgrath:程序员不喜欢被强迫做任何事情,尤其是有如此高的可维护成本(你必须在各个地方更改现有的代码)和相对较少的收益。如果你这样做,你的团队会反感你。 - C. K. Young
3
另一方面,过度使用检查型异常也可能导致糟糕的编码实践,比如捕获 Exception 或将 Exception 声明为被抛出。 - Stephen C
5
@Chris: 在我看来,传播SQLException更糟糕,因为它会暴露你的DAO实现内部。如果我决定切换到使用其他存储机制怎么办?最好将SQLException转换为与您的应用程序相关的业务异常。 - Adamski
1
@Will,捕获NullPointerException并不明确。最好捕获UserNotExistsException。只需观察catch块,您就知道发生了什么。 - santiagobasulto
显示剩余4条评论

8

虽然会有一些性能成本,但这并不是主要问题。

你真的想让你的代码像你的例子一样充斥着catch块吗?那太可怕了。

通常情况下,Web应用程序都有一个主异常处理程序,所有未捕获的异常都会进入该程序。至少这样,在出现错误时,处理过程会被干净地中断。如果有太多的异常捕获,你的流程就会像跌落几层楼梯一样。

有些异常是非常特殊的情况,你可以预见并处理它们。然而,通常情况下,异常出现在意外或无法控制的情况下。从那时起,你的对象可能处于糟糕的状态,因为后续步骤会期望某些未发生异常的东西存在,而尝试继续执行只会触发更多的异常。最好让意外的异常去自由地被中央处理程序捕获。


5

个人而言,我认为那是一个糟糕和讨厌的想法。

鼓励人们检查null值的通常方法是使用注释如@Nullable(及其相反的,对于保证返回非null的函数,则使用@NotNull)。通过在参数上设置类似的注释(以设置参数期望),优质的IDE和Bug-checker(如FindBugs)可以在代码不足够检查时生成所有必要的警告。

这些注释在JSR-305中可用,显然还有一种参考实现


就性能而言,创建异常是昂贵的部分(我读过这是由于堆栈跟踪填充等原因)。抛出异常是廉价的,并且是JRuby中使用的一种控制转移技术


2
Scala 也使用此方法来模拟 Java 中的 'break' 关键字(在这里看 Breaks.scala 和 NoStackTrace.scala:http://lampsvn.epfl.ch/trac/scala/browser/scala/trunk/src/library/scala/util/control)。 - Ben Lings
如果创建异常很昂贵,但抛出异常很便宜,那么这是否意味着抛出异常自动变得昂贵?因为你实际上需要创建一个异常来抛出。 - corgrath
1
@corgrath:如果你想要抛出一个真正的异常,那么成本是存在的。如果你将异常作为控制转移机制使用,你可以创建一个异常对象并存储它,然后在各个地方抛出同一个对象。由于它只用于控制转移,你不关心回溯信息的准确性,所以这种方法可行。 - C. K. Young
1
@corgrath 不,这两者可以解耦。例如,在某些异常的精确应用中,我们可能根本不关心堆栈跟踪。因此,我们可以构建一个存储异常实例(例如在静态成员中),并根据需要抛出它! :-) - KLE

5
一般来说,在Java中,异常非常昂贵,不应该用于流程控制!
异常的目的是描述一些特殊情况,例如NumberIsZeroException并不是真正的特殊情况,而PlanesWingsJustDetachedException显然是真正的特殊情况。如果在您的软件中,用户为null是真正的特殊情况,因为存在数据损坏或ID号码不匹配等问题,则可以使用异常。
异常还会导致与“快乐路径”偏离。虽然这不适用于您的示例代码,但有时使用Null Object而不是返回简单的null非常有益。

当Java还年轻的时候,我试图忽略数组边界检查,因为我认为ArrayIndexOutOfBoundsException会替我做这个工作。当我添加了简单的if (x<0)逻辑后,代码的运行速度大约提高了30倍,即使有八个if语句也是如此。内存使用也大大降低。虽然我最近没有进行性能测试,但我怀疑你在今天也会遇到类似的减速情况。 - Alex Feinman

4
建立堆栈跟踪大约需要一千个基本指令,它有明显的成本。
即使你没有要求,很多人可能会告诉你,你所设想的方法并不是很有吸引力... :-(
你强制要求调用者使用的代码真的很丑,难以编写和维护。
更具体地说,为了让人理解,我将尝试强调几个要点。
  • 如果文档清楚地说明了,那么其他开发人员负责检查从你的 API 接收到的用户是否为空。他的代码经常在内部执行这样的检查,因此在调用您的 API 后也可以进行检查。更重要的是,这将为他的代码增加一些统一性。

  • 当适当时,重用现有异常比创建自己的异常要好得多。例如,如果调用你的 API 并请求一个不存在的用户是一个错误,你可以抛出 IllegalArgumentException 异常。


1
抛出的异常处理起来是有代价的,但未抛出的异常是免费的。零成本异常是JVM的一个特性。 - Will
@Will 你的意思是,当我创建一个异常实例但不抛出它时是免费的吗?就像 new Exception(); 这样?你有官方的信息来源吗? - KLE
代码的正常路径不应该触发异常。零成本异常意味着,因为代码在其他情况下可能会抛出异常,所以这条路径不会变慢。在基于异常的开发中,异常仍然是特殊情况。因此,使用EDD通常不会对性能产生明显影响。 - Will
@Will:我想你说的是C++,不是Java。:-P 在Java中,创建异常的成本很高,而抛出/捕获异常则很便宜。请参见http://blogs.oracle.com/jrose/entry/longjumps_considered_inexpensive。 - C. K. Young
1
@Will,请注意,我尊重地提醒您,我的答案中提到的成本是构建堆栈跟踪的成本,而不是使用try-catch的成本。 - KLE

2

请参考这个答案,了解有关异常性能的信息。

基本上,您的想法是将RuntimeException、NullPointerException包装成已检查的异常;在我看来,问题可以在业务层面上通过ObjectNotFoundException进行处理:您没有找到用户,用户为空并导致错误发生。


1

我喜欢这种编码风格,因为它非常清楚地说明了从使用你的API的人的角度来看发生了什么。有时候,我甚至会将API方法命名为getMandatoryUser(String)getUserOrNull(String),以区分永远不会返回null的方法和可能返回null的方法。

关于性能,除非你在编写非常延迟关键的代码,否则性能开销将是微不足道的。然而,值得注意的是,在处理try / catch块时有一些最佳实践。例如,我相信Effective Java主张在循环外部创建try / catch块以避免在每次迭代时创建它;例如:

boolean done = false;

// Initialise loop counter here to avoid resetting it to 0 if an Exception is thrown.
int i=0;    

// Outer loop: We only iterate here if an exception is thrown from the inner loop.
do {
  // Set up try-catch block.  Only done once unless an exception is thrown.    
  try {
    // Inner loop: Does the actual work.
    for (; i<1000000; ++i) {
      // Exception potentially thrown here.
    }

    done = true;
  } catch(Exception ex) {
    ... 
  }
} while (!done);

1
创建异常、抛出异常(带有堆栈跟踪)、捕获异常,然后垃圾回收该异常(最终)比进行简单的 if 检查要慢得多。
最终,你可能可以将其归结为风格问题,但我认为这是非常糟糕的风格。

1

空对象可能对您有所帮助。前几天我在阅读马丁·福勒的书《重构》时,他描述了使用一个特定的对象来代替返回 null 的做法,这样可以避免您一直检查 NULL。

我不会在这里解释它,因为书中已经讲得非常清楚了。


@Tony 和 @Oscar ReyesNullObjects 很有趣,但是如果用户为空,我需要中止操作,因为通常我会对 User 对象进行更多的操作。如果我要使用 NullObjects,比如 NullUser,我需要使用 "user instanceof NullUser" 来确定该名称的用户是否存在。 - corgrath
1
如果需要在它为null时中止,为什么不保持原样,让NullPointerException一直流到一个终止进程的catch块(当用户为null或任何其他请求的对象为null时...当有一个实际上表示你想要的意思的异常时,没有必要填充更多的异常。 - David Rodríguez - dribeas
我理解这一点,但是假设我想获取用户和车辆,两者都可能引发NullPointerException - 那么你怎么知道哪一个出了问题?那样的话,抛出UserIsNullException和VehicleIsNullException不是更好吗?在这种情况下,您可以进行适当的错误处理。 - corgrath

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