在Java中编写“异常驱动开发”的性能成本?

Performance cost of coding “exception driven development” in Java?

在Java中创建、抛出和捕获异常是否有任何性能开销?

我计划将"异常驱动开发"添加到一个更大的项目中。我想设计自己的异常并将它们包含到我的方法中,迫使开发人员捕获并做适当的工作。

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

1
public User getUser(String name);

但是,用户可能为空,在使用用户的公共方法之前,通常忘记检查这一点。

1
2
3
User user = getUser("adam");

int age = user.getAge();

这将导致NullPointerException和崩溃。但是,如果在返回用户对象之前进行了检查,如果该对象为空并引发"userIsNullException":

1
public User getUser(String name) throws UserIsNullException;

我强迫实施者思考和行动:

1
2
3
4
5
6
7
8
9
try {

    User user = getUser("adam");

    int age = user.getAge();

}catch( UserIsNullException e) {

}

它使代码对于意外崩溃更加安全,并消除了更多的错误。假设该网站每小时有数百名访问者,而且这种设计模式在几乎所有地方都被使用。

这种设计方法将如何影响性能?好处是权衡成本还是仅仅是简单的错误编码?

谢谢你的帮助!

更新!很明显,我的注意并不是像我的示例所建议的那样包装一个空指针异常。目标是强制实现者编写一个try/catch,从而避免真正崩溃时的头疼,因为:

用户=空

被遗忘了。问题在于比较这两种设计模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
int age;

try {

User user = getUser("adam");

age = user.getAge();

}catch( UserIsNullException e) {

age = 0;

}

对比:

1
2
3
4
5
6
7
8
9
int age;

User user = getUser("adam");

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


"which would result in
NullPointerException and a crash."

NullPointerException是一个异常,可以在catch中捕获。

因此,额外的异常是多余的,除非它增加了代码的清晰度。在这个例子中,它确实不存在。事实上,您刚刚接受了一个未检查的程序员错误异常,并将其升级为检查的异常。

为程序员的错误创建一个检查过的异常实际上意味着您的代码必须显式地处理程序员在没有错误时引入错误的可能性。


抛出异常会有一个性能惩罚,但这通常是可以接受的,因为异常处理代码只在异常情况下执行。如果您开始为流控制使用异常,那么您将改变通常的情况并使它们成为预期的行为。我强烈建议不要这样做。


有一个性能成本,但这不是主要问题。你真的想让你的代码像你的例子那样到处都是catch块吗?太可怕了。

通常,Web应用程序有一个主要的异常处理程序,其中所有未捕获的内容都会结束,至少在出现错误时,这种处理会被完全切断。除了所有这些例外,在你的流程中捕捉就像从几段楼梯上摔下来一样。

有些例外是您所期望和可以处理的非常特殊的情况。然而,一般情况下,当意外或无法控制的事情发生错误时,会弹出异常。从那时起,您的对象可能处于糟糕的状态,因为随后的步骤将期望出现由于异常而没有发生的情况,并且尝试继续只会触发更多的异常。最好让意外的异常消失,这样它们就可以被中心处理程序捕获。


一般来说,Java中的异常非常昂贵,不应该用于流量控制!

例外的要点是描述一些特殊的东西,例如NumberIsZeroException并不是真正的例外,而PlanesWingsJustDetachedException显然是真正的例外。如果您的软件中的用户是null,因为存在数据损坏或ID号不匹配之类的情况,那么可以使用例外。

例外也导致了与"快乐之路"的背离。虽然这不适用于示例代码,但有时使用空对象而不是返回普通的null是非常有益的。


就我个人而言,我认为这是一个糟糕而令人讨厌的想法。

鼓励人们检查空值的通常方法是对保证返回非空值的函数使用诸如@Nullable之类的注释(与之相反的是,@NotNull)。通过在参数上设置类似的注释(以便设置参数期望值),当代码没有进行足够的检查时,质量IDE和bug检查程序(如findbugs)可以生成所有必要的警告。

这些注释在JSR-305中可用,显然还有一个引用实现。

就性能而言,创建异常是代价高昂的部分(我已经了解到这是由于stacktrace填充等原因)。抛出异常很便宜,是JRuby中使用的一种控制传输技术。


构建一个堆栈跟踪大约需要1000条基本指令。它有一定的成本。

即使你没有问,很多人可能会告诉你,你设想的方法似乎不太吸引人…:

您强制呼叫者使用的代码非常难看,难以编写和维护。

更具体一点,试着理解,我会强调几点。

  • 另一个开发人员负责检查从您的API接收到的用户的无效性(如果文档中有明确的说明)。他的代码定期进行内部检查,因此他也可以在调用您的API之后进行检查。更重要的是,这将使他的代码具有某种同质性。

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


空对象可能对您有所帮助。前几天我在读马丁·福勒的《重构》一书,书中描述了如何使用一个特定的对象来代替返回一个空值,从而避免了你必须一直检查空值。

我不想在这里解释,因为书中描述得很好。


创建一个异常,抛出一个异常(带有堆栈跟踪),捕获一个异常,然后垃圾收集该异常(最终)要比执行简单的if检查慢得多。

归根结底,你可以把它归结为风格,但我认为这是非常糟糕的风格。


我喜欢这种类型的编码,因为它从使用API的人的角度非常清楚地说明了正在发生的事情。有时我甚至命名api方法getMandatoryUser(String)getUserOrNull(String)来区分永远不会返回null的方法和返回null的方法。

关于性能,除非您正在编写一段非常滞后的关键代码,否则性能开销是可以忽略的。但是,值得一提的是,在尝试/捕获块时有一些最佳实践。例如,我相信有效的Java倡导者在循环之外创建一个尝试/ catch块,以避免在每次迭代中创建它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);

请参阅有关性能和例外情况的回答。

基本上,在您的想法中,您正在将RuntimeException(NullPointerException)包装为选中的异常;imho我想说,可以在业务级别使用ObjectNotFoundException来管理问题:您找不到用户,用户为空,这会产生错误。


异常成本很高,因为每次抛出异常时,都必须创建和填充堆栈跟踪。

设想一个余额转移操作在1%的情况下由于资金不足而失败。即使故障率相对较低,性能也可能受到严重影响。有关源代码和基准测试结果,请参见此处。


根据你上次的编辑。我认为(正如托尼建议的那样)使用nullobject模式在这里更有用。

考虑第三种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class NullUser extends User {

    public static NullUser nullUser = new NullUser();

    private NullUser(){}

    public int getAge() {
        return 0;
    }
}

//Later...
int age;

User user = getUser("adam"); // return nullUser if not found.
age = user.getAge();         // no catch, no if/null check, pure polymorphism

您可以忘记异常并避免这种性能问题。这使得您的API可以自己说话。

1
2
3
4
5
6
7
8
9
10
11
int getUserAge(String userName)
{
    int age = 0;
    UserSearchResult result = getUser(userName);
    if (result.getFoundUser())
    {
        User user = result.getUser();
        age = user.getAge();
    }
    return age;
}

How would such a design approach
effect performance? Do the benefits
out-weigh the cost or is it just plain
bad coding?

我说过,这种编程风格几乎没有任何好处。例外应理解为(以及…)例外。它应该只在非正常情况下发生。你如何定义"正常情况"是一个有争议的想法…