56

在 Java 中使用try/catch块是否有任何开销,而不是if 块(假设封闭的代码不这样做)?

例如,采用以下两个简单的字符串“安全修剪”方法实现:

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,这两种方法之间是否有任何性能差异

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

例如,一个带有 的方法是一种常见的情况N parameters,它在开始时使用M <= N它们,如果任何此类参数是“无效的”(例如,null 或空字符串),它会快速且确定性地失败,而不会影响其余的代码。

在这种情况下,不必编写k * M if 块(其中k是每个参数的平均检查次数,例如k = 2对于 null 或空字符串),try/catch块将显着缩短代码,并且可以使用 1-2 行注释明确指出“非常规”逻辑。

这种模式也将加速该方法,特别是如果错误条件很少发生,并且它会在不损害程序安全性的情况下这样做(假设错误条件是“正常的”,例如在字符串处理方法中,其中 null 或空值是可以接受的,尽管很少出现)。

4

5 回答 5

65

我知道你问的是性能开销,但你真的不应该使用try/catchif互换。

try/catch用于出现超出您控制范围且不在正常程序流程中的问题。例如,尝试写入文件并且文件系统已满?这种情况通常应该使用try/来处理catch

if语句应该是正常的流程和普通的错误检查。那么,例如,用户未能填充必需的输入字段?用于if此,而不是try/ catch

在我看来,您的示例代码强烈建议正确的方法是if声明而不是try/ catch

为了回答您的问题,我推测 a try/中的开销通常catch比a 更多if。要确定,请获取 Java 分析器并找出您关心的特定代码。答案可能会因情况而异。

于 2011-12-24T00:18:07.233 回答
34

这个问题几乎被“回答到死”了,但我认为还有几点可以有用:

  • 用于try / catch非异常控制流是不好的风格(在 Java 中)。(经常有关于“非异常”是什么意思的争论……但这是一个不同的话题。)

  • 它的风格不好的部分原因是它try / catch比常规的控制流语句1贵几个数量级。实际差异取决于程序和平台,但我预计它会贵 1000 倍或更多倍。除其他外,异常对象的创建捕获堆栈跟踪,查找和复制有关堆栈上每个帧的信息。堆栈越深,需要复制的内容就越多。

  • 风格不好的另一部分原因是代码更难阅读。

1 - 最近版本的 Java 中的 JIT 可以优化异常处理,以在某些情况下大幅减少开销。但是,默认情况下不启用这些优化。

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

  • 捕捉Exception是非常糟糕的做法,因为您有可能会意外捕捉到其他未经检查的异常。例如,如果您在调用时这样做,raw.substring(1)您也会捕获潜在StringIndexOutOfBoundsException的 s ... 并隐藏错误。

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


我的问题更笼统,不仅仅是null价值观。

...而且我回答中的大部分观点都与null价值观无关!

但请记住,在许多情况下,您希望允许偶尔出现空值或任何其他可能产生异常的值,然后忽略它们。例如,当从某个地方读取键/对值并将它们传递给像上面的 tryTrim() 这样的方法时,就是这种情况。

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

但我会争辩说,tryTrim()正在做的事情(通常)是错误的处理方式null。比较这三位代码:

// 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()本质上是坏的。但是,您觉得需要编写这样的方法这一事实可能表明空值处理不太理想。

最后,还有其他方法可以在 API 中对“没有价值”进行建模。这些包括:

  • 测试nullsetter 和构造函数中的意外情况,然后抛出异常或替换不同的值。
  • 添加一个方法...如果结果是. 则is<Field>Set抛出异常。get<Field>null
  • 使用空对象模式
  • 使用空字符串、空集合、零长度数组等代替null字符串、集合或数组1
  • 使用Optional.

1 - 请注意,这与 Null 对象模式不同,因为不一定只有一个类型的实例来表示“nullity”。但是您可以将这两个想法结合起来……如果您对此有纪律的话。

于 2011-12-24T02:20:55.910 回答
11

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

在谈到这个话题时,不要Exception在这里抓住它,尤其是不要吞下它。在你的情况下,你会期望一个NullPointerException. 如果你要抓到什么,那就是你会抓到的(但回到第一段,要这样做)。当你抓住(并吞下!)Exception时,你是在说“不管出了什么问题,我都能处理。我不在乎它是什么。” 您的程序可能处于不可撤销的状态!只捕获您准备处理的内容,让其他所有内容传播到可以处理它的层,即使该层是顶层并且它所做的只是记录异常然后点击弹出开关。

于 2011-12-24T00:18:20.957 回答
5

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

于 2011-12-24T00:18:53.777 回答
-3

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

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();
}

}

我得到的数字是:

Try time:8102
If time:1956

至于什么风格 - 这是一个完全独立的问题。if 语句看起来很自然,但是由于多种原因,try 看起来很奇怪: - 即使您正在检查 NULL 值,您也捕获了异常,您是否期望发生“异常”(否则捕获 NullException)?- 你发现了那个异常,你是要报告还是吞下?等等等等等等

编辑:请参阅我的评论,了解为什么这是一个无效的测试,但我真的不想把它留在这里。只是通过交换 tryTrim 和 ifTrim,我们突然得到以下结果(在我的机器上):

Try time:2043
If time:6810

与其开始解释所有这些,不如从头开始阅读- Cliff 也有一些关于整个主题的精彩幻灯片,但我目前找不到链接。

了解异常处理在 Hotspot 中的工作原理后,我相当确定在正确的测试中,try/catch 没有异常)将是基准性能(因为没有任何开销),但是 JIT 可以通过 Nullpointer 检查发挥一些技巧,然后方法调用(没有显式检查,但如果对象为空则捕获硬件异常)在这种情况下,我们会得到相同的结果。也不要忘记:我们谈论的是一个容易预测的差异,如果它是一个 CPU 周期!修剪电话将花费一百万倍。

于 2011-12-24T00:33:12.623 回答