关于异常处理:Java if与try / catch开销

Java if vs. try/catch overhead

Java是否有使用TIG/catch块的开销,而不是IF块(假设封闭代码不要求这样)?

例如,对字符串采用以下两种简单的"安全修剪"方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public String tryTrim(String raw) {
    try {
        return raw.trim();
    } catch (Exception e) {
    }
    return null;
}

public String ifTrim(String raw) {
    if (raw == null) {
        return null;
    }
    return raw.trim();
}

如果raw输入很少是null输入,这两种方法的性能有什么区别吗?

此外,使用tryTrim()方法简化代码布局是否是一种良好的编程模式,特别是当许多if块通过将代码封闭在一个try/catch块中可以避免检查罕见错误条件时?

例如,在不影响代码其余部分的情况下,使用N parameters的方法是一种常见的情况,该方法在接近开始时使用M <= N,如果任何此类参数"无效"(例如,空字符串或空字符串),则会快速而确定性地失败。

在这种情况下,如果块(其中,k是每个参数的平均检查次数,例如,k = 2是空字符串或空字符串),则不必编写k * Mif块,try/catch块将显著缩短代码,并且可以使用1-2行注释来显式地注意"非常规"逻辑。

这样的模式也会加快方法的速度,特别是在错误条件很少出现的情况下,而且这样做不会影响程序的安全性(假设错误条件是"正常的",例如在字符串处理方法中,可以接受空值或空值,尽管很少出现)。


我知道你在问性能开销,但是你真的不应该交替使用try/catchif

try/catch用于发生错误的事情,这些事情超出了您的控制范围,不在正常的程序流中。例如,尝试写入文件,但文件系统已满?这种情况通常应该用trycatch来处理。

if语句应为正常流和一般错误检查。例如,用户无法填充一个必需的输入字段?用if表示,不用trycatch表示。

在我看来,您的示例代码强烈建议使用正确的方法,这里有一个if语句,而不是trycatch

为了回答你的问题,我猜想一个try/catch通常比一个if有更多的开销。要知道,获取一个Java剖析器并找出你关心的特定代码。答案可能因情况而异。


这个问题几乎是"死而复生",但我认为还有几点可以有用地提出:

  • 对于非异常控制流使用EDCOX1 0是坏的风格(在Java中)。(经常有关于"非特殊"的含义的争论……但这是另一个话题。)

  • 它不好的部分原因是try / catch比常规的控制流声明1昂贵很多。实际的区别取决于程序和平台,但我预计它的价格会高出1000倍或更多。除其他事情外,创建异常对象捕获一个堆栈跟踪,查找和复制关于堆栈上每个帧的信息。堆栈越深,需要复制的就越多。

  • 另一部分原因是它的样式不好,因为代码更难阅读。

1——Java 7的最新版本中的JIT可以在某些情况下优化异常处理以大幅降低开销。但是,默认情况下不会启用这些优化。

您编写示例的方式也存在问题:

  • 抓到EDOCX1[2]是非常糟糕的做法,因为您有可能意外地抓到其他未经检查的异常。例如,如果你在给raw.substring(1)打电话时那样做,你也会抓住潜在的StringIndexOutOfBoundsException的……隐藏虫子。

  • 您的示例所要做的是(可能)处理null字符串的不好实践的结果。作为一般原则,您应该尽量减少使用null字符串,并尝试限制它们(有意)的传播。在可能的情况下,使用空字符串而不是null表示"无值"。当您确实需要传递或返回一个null字符串时,请在方法javadocs中清楚地记录它。如果您的方法在不应该调用时使用null进行调用…这是一个错误。让它抛出一个异常。不要试图通过(在本例中)返回null来补偿错误。

追随

My question was more general, not only for null values.

…我答案中的大多数点都不是关于空值的!

But please bear in mind that there are many cases where you want to allow the occasional null value, or any other value that could produce an exception, and just ignore them. This is the case, for example, when reading key/pair values from somewhere and passing them to a method like the tryTrim() above.

是的,在某些情况下,预期会有null值,您需要处理它们。

但我认为,tryTrim()所做的(通常)是处理null的错误方法。比较这三位代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Version 1
String param = httpRequest.getParameter("foo");
String trimmed = tryTrim(param);
if (trimmed == null) {
    // deal with case of 'foo' parameter absent
} else {
    // deal with case of 'foo' parameter present
}

// Version 2
String param = httpRequest.getParameter("foo");
if (param == null) {
    // deal with case of 'foo' parameter absent
} else {
    String trimmed = param.trim();
    // deal with case of 'foo' parameter present
}

// Version 3
String param = httpRequest.getParameter("foo");
if (param == null) {
    // treat missing and empty parameters the same
    param ="";
}
String trimmed = param.trim();

最后,您必须以不同于常规字符串的方式处理null,而且通常最好尽快这样做。允许null从其点源传播的越远,程序员越可能忘记null值是可能的,并编写假定为非空值的错误代码。忘记HTTP请求参数可能丢失(即param == null)是发生这种情况的典型情况。

我不是说tryTrim()天生就不好。但事实上,程序员认为这样的写方法是必要的,这可能意味着不太理想的空处理。


使用第二个版本。当其他替代方案可用时,不要使用控制流的异常,因为这不是它们的用途。例外情况适用于特殊情况。

在这个话题上,不要在这里抓住Exception,尤其不要吞咽它。在你的情况下,你会期望一个NullPointerException。如果你要抓住什么,那就是你要抓住的(但回到第一段,不要这样做)。当你抓到(并吞下!)以东十一〔17〕你说:"无论出什么事,我都能处理。我不在乎它是什么。"你的程序可能处于不可撤销的状态!只捕获您准备处理的内容,让其他所有内容应用到可以处理它的层,即使该层是顶层,它所做的就是记录异常,然后点击弹出开关。


否则,异常的抛出和捕获速度很快(虽然if可能仍然更快),但缓慢的事情是创建异常的堆栈跟踪,因为它需要遍历所有当前堆栈。(一般来说,将异常用于控制流是不好的,但是当确实需要异常并且异常必须很快时,可以通过重写Throwable.fillInStackTrace()方法跳过构建堆栈跟踪,或者保存一个异常实例并反复抛出,而不是总是创建一个新的异常实例。)


就开销而言,您可以自己测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Overhead {

public static void main(String[] args) {
    String testString ="";

    long startTimeTry = System.nanoTime();
    tryTrim(testString);
    long estimatedTimeTry = System.nanoTime() - startTimeTry;

    long startTimeIf = System.nanoTime();
    ifTrim(testString);
    long estimatedTimeIf = System.nanoTime() - startTimeIf;

    System.out.println("Try time:" + estimatedTimeTry);
    System.out.println("If time:" + estimatedTimeIf);

}

public static String tryTrim(String raw) {
    try {
        return raw.trim();
    } catch (Exception e) {
    }
    return null;
}

public static String ifTrim(String raw) {
    if (raw == null) {
        return null;
    }
    return raw.trim();
}

}

我得到的数字是:

1
2
Try time:8102
If time:1956

至于风格,这是一个完全独立的问题。if语句看起来很自然,但是尝试看起来很奇怪,原因有很多:-您捕获了异常,即使您正在检查空值,您是否希望发生"异常"情况(否则捕获NullException)?-你抓住了那个例外,你打算报告还是吞下?等。

编辑:看看我的评论,为什么这是一个无效的测试,但我真的不想让这个站在这里。只需交换trytrim和iftrim,我们就会突然得到以下结果(在我的机器上):

1
2
Try time:2043
If time:6810

不是开始解释所有这些,只是从头读这个-克里夫也有一些关于整个主题的伟大幻灯片,但我现在找不到链接。

知道异常处理如何在热点中工作,我相当确定,在正确的测试中,无异常的Try/Catch将是基线性能(因为没有任何开销),但是JIT可以在空指针检查后进行方法调用(没有显式检查,但如果对象在这种情况下,我们会得到相同的结果。另外,不要忘记:我们讨论的是一个容易预测的差异,如果哪一个是一个CPU周期的话!修剪电话的费用是这个的一百万倍。