应该尝试…在循环内还是循环外捕捉?


184

我有一个看起来像这样的循环:

for (int i = 0; i < max; i++) {
    String myString = ...;
    float myNum = Float.parseFloat(myString);
    myFloats[i] = myNum;
}

这是方法的主要内容,其唯一目的是返回浮点数数组。我希望此方法null在出现错误时返回,因此我将循环放在一个try...catch块中,如下所示:

try {
    for (int i = 0; i < max; i++) {
        String myString = ...;
        float myNum = Float.parseFloat(myString);
        myFloats[i] = myNum;
    }
} catch (NumberFormatException ex) {
    return null;
}

但是后来我也想到将try...catch块放入循环中,如下所示:

for (int i = 0; i < max; i++) {
    String myString = ...;
    try {
        float myNum = Float.parseFloat(myString);
    } catch (NumberFormatException ex) {
        return null;
    }
    myFloats[i] = myNum;
}

是否出于某种原因(无论是性能还是其他原因)偏爱一个?


编辑:共识似乎是将循环放在try / catch中(可能在其自己的方法中)更干净。但是,仍然存在关于哪个更快的争论。有人可以对此进行测试并返回统一答案吗?


2
我不了解性能,但是在for循环之外使用try catch可以使代码看起来更整洁。
Jack B Nimble

Answers:


132

性能:

在try / catch结构的放置位置上绝对没有性能差异。在内部,它们被实现为在调用该方法时创建的结构中的代码范围表。在执行该方法时,除非抛出异常,否则try / catch结构完全不在画面之内,然后将错误的位置与表进行比较。

这是参考:http : //www.javaworld.com/javaworld/jw-01-1997/jw-01-hood.html

该表大约在中途被描述。


好吧,所以您是说除了美学之外没有其他区别。您可以链接到源吗?
迈克尔·迈尔斯

2
谢谢。我想自1997年以来情况可能有所变化,但几乎没有降低效率。
迈克尔·迈尔斯

1
绝对是个硬汉。在某些情况下,它可能会抑制编译器的优化。这不是直接成本,但可能是间接的性能损失。
汤姆·霍顿

2
是的,我想我应该有点热情,但是错误的信息让我感到不安!在优化的情况下,由于我们不知道会以何种方式影响性能,因此我猜想又回到了“尝试并测试”的状态。
Jeffrey L Whitledge,

1
仅在代码无例外运行的情况下,性能没有差异。
OnurBıyık2010年

72

性能:正如Jeffrey在回复中所说的那样,在Java中并没有太大的区别。

通常,为了代码的可读性,对捕获异常的位置的选择取决于您是否希望循环继续进行处理。

在您的示例中,您在捕获到异常后返回。在那种情况下,我会把try / catch放在循环中。如果您只是想获取一个不好的值而继续进行处理,请将其放入其中。

第三种方式:您始终可以编写自己的静态ParseFloat方法,并在该方法而不是循环中处理异常处理。使异常处理与循环本身隔离!

class Parsing
{
    public static Float MyParseFloat(string inputValue)
    {
        try
        {
            return Float.parseFloat(inputValue);
        }
        catch ( NumberFormatException e )
        {
            return null;
        }
    }

    // ....  your code
    for(int i = 0; i < max; i++) 
    {
        String myString = ...;
        Float myNum = Parsing.MyParseFloat(myString);
        if ( myNum == null ) return;
        myFloats[i] = (float) myNum;
    }
}

1
仔细看 在这两者的第一个例外中,他都将退出。一开始我也想过同样的事情。
凯文

2
我知道,这就是为什么我要对他说,因为他遇到问题时会回来,所以我会使用外部捕获。为了清楚起见,这就是我要使用的规则……
Ray Hayes

好的代码,但是不幸的是Java没有out参数。
迈克尔·迈尔斯

ByRef?自接触Java以来​​已有很长时间了!
Ray Hayes

2
有人建议使用TryParse,它在C#/。net中可让您进行安全的解析而无需处理异常,现在已将其复制并且该方法可重用。至于原来的问题,我宁愿捕捉里面的循环,它只是看起来更清洁,即使不存在性能问题..
雷海耶斯

46

好的,在Jeffrey L Whitledge说没有性能差异(截至1997年)之后,我去测试了它。我运行了这个小基准:

public class Main {

    private static final int NUM_TESTS = 100;
    private static int ITERATIONS = 1000000;
    // time counters
    private static long inTime = 0L;
    private static long aroundTime = 0L;

    public static void main(String[] args) {
        for (int i = 0; i < NUM_TESTS; i++) {
            test();
            ITERATIONS += 1; // so the tests don't always return the same number
        }
        System.out.println("Inside loop: " + (inTime/1000000.0) + " ms.");
        System.out.println("Around loop: " + (aroundTime/1000000.0) + " ms.");
    }
    public static void test() {
        aroundTime += testAround();
        inTime += testIn();
    }
    public static long testIn() {
        long start = System.nanoTime();
        Integer i = tryInLoop();
        long ret = System.nanoTime() - start;
        System.out.println(i); // don't optimize it away
        return ret;
    }
    public static long testAround() {
        long start = System.nanoTime();
        Integer i = tryAroundLoop();
        long ret = System.nanoTime() - start;
        System.out.println(i); // don't optimize it away
        return ret;
    }
    public static Integer tryInLoop() {
        int count = 0;
        for (int i = 0; i < ITERATIONS; i++) {
            try {
                count = Integer.parseInt(Integer.toString(count)) + 1;
            } catch (NumberFormatException ex) {
                return null;
            }
        }
        return count;
    }
    public static Integer tryAroundLoop() {
        int count = 0;
        try {
            for (int i = 0; i < ITERATIONS; i++) {
                count = Integer.parseInt(Integer.toString(count)) + 1;
            }
            return count;
        } catch (NumberFormatException ex) {
            return null;
        }
    }
}

我使用javap检查了生成的字节码,以确保没有内联。

结果表明,假设微不足道的JIT优化,Jeffrey是正确的Java 6,Sun客户端VM上绝对没有性能差异(我无法访问其他版本)。在整个测试过程中,总时间差约为几毫秒。

因此,唯一的考虑就是看起来最干净的东西。我发现第二种方法很难看,所以我会坚持第一种方法或Ray Hayes的方法


如果异常处理程序将流程带到循环外部,那么我认为将其放置在循环之外最明显-而不是将catch子句显然放在循环中,尽管如此,该子句仍会返回。我在这种“全部捕获”方法的“捕获”主体周围使用了一行空格,以更清楚地将主体全封闭try块分开。
Thomas W

16

虽然性能可能是相同的,并且“外观”更好的是主观的,但功能上仍存在很大差异。请看以下示例:

Integer j = 0;
    try {
        while (true) {
            ++j;

            if (j == 20) { throw new Exception(); }
            if (j%4 == 0) { System.out.println(j); }
            if (j == 40) { break; }
        }
    } catch (Exception e) {
        System.out.println("in catch block");
    }

while循环在try catch块内部,变量'j'递增直到其达到40,当j mod 4为零时将其打印出来,并且当j达到20时将引发异常。

在详细说明之前,这里是另一个示例:

Integer i = 0;
    while (true) {
        try {
            ++i;

            if (i == 20) { throw new Exception(); }
            if (i%4 == 0) { System.out.println(i); }
            if (i == 40) { break; }

        } catch (Exception e) { System.out.println("in catch block"); }
    }

与上述逻辑相同,唯一的区别是try / catch块现在位于while循环内。

这是输出(在try / catch中):

4
8
12 
16
in catch block

和另一个输出(try / catch in while):

4
8
12
16
in catch block
24
28
32
36
40

那里您有很大的不同:

在try / catch中打破循环

尝试/捕捉同时保持循环活动


因此try{} catch() {},如果将循环视为一个“单元”,则将其放回原处,并在该单元中发现问题会使整个“单元”(在本例中为循环)向南移动。try{} catch() {}如果将循环的单个迭代或数组中的单个元素视为一个整体“单元”,则将其放入循环中。如果该“单元”中发生异常,我们将跳过该元素,然后继续进行循环的下一个迭代。
银河

14

我同意所有的性能和可读性职位。但是,在某些情况下,它确实很重要。其他人也提到了这一点,但是通过示例可能更容易看到。

请考虑以下示例:

public static void main(String[] args) {
    String[] myNumberStrings = new String[] {"1.2345", "asdf", "2.3456"};
    ArrayList asNumbers = parseAll(myNumberStrings);
}

public static ArrayList parseAll(String[] numberStrings){
    ArrayList myFloats = new ArrayList();

    for(int i = 0; i < numberStrings.length; i++){
        myFloats.add(new Float(numberStrings[i]));
    }
    return myFloats;
}

如果您希望parseAll()方法在出现任何错误时返回null(例如原始示例),则可以将try / catch放在外部,如下所示:

public static ArrayList parseAll1(String[] numberStrings){
    ArrayList myFloats = new ArrayList();
    try{
        for(int i = 0; i < numberStrings.length; i++){
            myFloats.add(new Float(numberStrings[i]));
        }
    } catch (NumberFormatException nfe){
        //fail on any error
        return null;
    }
    return myFloats;
}

实际上,您可能应该在这里返回一个错误而不是null,并且通常我不喜欢多次返回,但是您知道了。

另一方面,如果您希望它仅忽略问题,并解析所有可能的String,则可以将try / catch放在循环内部,如下所示:

public static ArrayList parseAll2(String[] numberStrings){
    ArrayList myFloats = new ArrayList();

    for(int i = 0; i < numberStrings.length; i++){
        try{
            myFloats.add(new Float(numberStrings[i]));
        } catch (NumberFormatException nfe){
            //don't add just this one
        }
    }

    return myFloats;
}

2
这就是为什么我说“如果您只是想捕获一个差值,然后继续进行处理,请将其放入其中。”
雷·海斯

5

如前所述,性能是相同的。但是,用户体验不一定相同。在第一种情况下,您将快速失败(即,在第一个错误之后),但是,如果将try / catch块放入循环中,则可以捕获为给定方法调用而将创建的所有错误。当从字符串中解析出值数组时,您可能会期望出现一些格式错误,在某些情况下,您肯定希望将所有错误呈现给用户,这样他们就不需要一一尝试修复它们了。 。


在这种情况下(我要在catch块中返回),情况并非如此,但总体上牢记这是一件好事。
迈克尔·迈尔斯

4

如果全部失败,则第一种格式有意义。如果您希望能够处理/返回所有非失败元素,则需要使用第二种形式。这些将是我在方法之间进行选择的基本标准。就个人而言,如果全有或全无,我将不使用第二种形式。


4

只要您知道在循环中需要完成的工作,就可以将try catch置于循环之外。但是重要的是要了解,循环将在异常发生后立即结束,而这不一定总是您想要的。这实际上是基于Java的软件中非常常见的错误。人们需要处理许多项目,例如清空队列,并且错误地依赖于外部try / catch语句来处理所有可能的异常。他们也可能只在循环内处理特定的异常,而不希望发生任何其他异常。然后,如果发生未在循环内部处理的异常,则该循环将被“预定义”,并可能过早结束,并且外部catch语句将处理该异常。

如果循环在其生活中扮演着清空队列的角色,那么该循环很可能在真正清空该队列之前结束。很常见的故障。


2

在您的示例中,没有功能上的差异。我发现您的第一个示例更具可读性。


2

您应该更喜欢外部版本而不是内部版本。这只是规则的特定版本,可以将任何内容移到循环外,也可以将其移到循环外。根据IL编译器和JIT编译器的不同,两个版本最终可能会或可能不会具有不同的性能特征。

另一方面,您可能应该看一下float.TryParse或Convert.ToFloat。


2

如果将try / catch放入循环内,则在发生异常后将继续循环。如果将其置于循环之外,则将在引发异常后立即停止。


好吧,我在异常上返回null,因此在我的情况下它不会继续处理。
迈克尔·迈尔斯

2

我的观点是,try / catch块对于确保适当的异常处理是必需的,但是创建此类块会影响性能。由于循环包含大量重复计算,因此不建议将try / catch块放入循环中。此外,似乎在这种情况发生的地方,通常是捕获到“异常”或“ RuntimeException”。应该避免RuntimeException被代码捕获。同样,如果您在大公司工作,则必须正确记录该异常,或者停止运行时异常的发生,这一点很重要。本说明的重点是PLEASE AVOID USING TRY-CATCH BLOCKS IN LOOPS


1

为try / catch设置特殊的堆栈框架会增加额外的开销,但是JVM可能能够检测到您正在返回的事实并对此进行了优化。

根据迭代次数,性能差异可能会忽略不计。

但是,我同意其他人的观点,即将其置于回路之外会使回路主体看起来更干净。

如果您有可能想要继续处理而不是在有无效数字的情况下退出,那么您将希望代码在循环内。


1

如果在内部,那么您将获得N次try / catch结构的开销,而不是外部的一次。


每次调用Try / Catch结构时,都会增加方法执行的开销。处理该结构只需要一点内存和处理器的滴答声。如果您运行了100次循环,并且出于假设的原因,假设每次尝试/捕获调用的成本为1滴答,那么在循环内使用“尝试/捕获”则要花费100滴答,而如果是,则仅需要1滴答在循环之外。


您能详细说明一下开销吗?
迈克尔·迈尔斯

1
好的,但是Jeffrey L Whitledge说没有额外的开销。您能否提供一个说明存在的来源?
迈克尔·迈尔斯

成本很小,几乎可以忽略不计,但是就在那儿-一切都有成本。下面是从程序员的天堂(一博客文章tiny.cc/ZBX1b)关于.NET和下面是Reshat Sabiq关于JAVA(新闻组后tiny.cc/BV2hS
斯蒂芬Wrighton

我看到了...所以现在的问题是,进入try / catch的成本是多少(在这种情况下,它应该在循环之外),还是在try / catch的持续时间内是恒定的(在这种情况下,它是应该在循环内)?您说它是第一名。我仍然不完全相信有有代价的。
迈克尔·迈尔斯

1
根据此Google图书(tinyurl.com/3pp7qj)的说法,“最新的虚拟机不会造成任何损失”。
迈克尔·迈尔斯

1

异常的全部目的是鼓励采用第一种方式:将错误处理合并并处理一次,而不是立即在每个可能的错误站点处处理。


错误!例外的要点是确定发生“错误”时应采取的异常行为。因此,选择内部或外部try / catch块实际上取决于您要如何处理循环处理。
gizmo

使用例外前错误处理,例如传递“发生错误”标志,同样可以很好地完成。
wnoise

1

放进去 您可以继续处理(如果需要),也可以抛出一个有用的异常,该异常告诉客户端myString的值以及包含错误值的数组的索引。我认为NumberFormatException已经可以告诉您错误的值了,但是原理是将所有有用的数据放在抛出的异常中。在程序的这一点上,考虑一下在调试器中您可能会感兴趣的地方。

考虑:

try {
   // parse
} catch (NumberFormatException nfe){
   throw new RuntimeException("Could not parse as a Float: [" + myString + 
                              "] found at index: " + i, nfe);
} 

在有需要的时候,您将非常欣赏这样的异常,其中包含尽可能多的信息。


是的,这也是有道理的。就我而言,我正在从文件中读取数据,因此,我真正需要的只是无法解析的字符串。所以我做了System.out.println(ex)而不是抛出另一个异常(我想解析合法的文件)。
迈克尔·迈尔斯

1

0.02c在查看在哪里放置异常处理的一般问题时,我想补充自己的两个竞争考虑因素:

  1. try-catch块的责任(即在您的情况下不在循环之内)意味着在稍后的某个时刻更改代码时,您可能会错误地添加一条由现有catch块处理的行;可能是无意的。在您的情况下,这种可能性较小,因为您明确捕获了NumberFormatException

  2. try-catch块的职责“越窄” ,重构就越困难。特别是(如您的情况)在catch块(return null语句)内执行“非本地”指令时。


1

这取决于故障处理。如果您只想跳过错误元素,请尝试内部:

for(int i = 0; i < max; i++) {
    String myString = ...;
    try {
        float myNum = Float.parseFloat(myString);
        myFloats[i] = myNum;
    } catch (NumberFormatException ex) {
        --i;
    }
}

在任何其他情况下,我都希望在室外尝试一下。代码更易读,更干净。如果返回null,则在错误情况下抛出IllegalArgumentException可能更好。


1

我将投入$ 0.02。有时您最终需要在代码中稍后添加一个“ finally”(因为谁曾经第一次完美地编写了他们的代码?)。在这些情况下,突然将try / catch置于循环之外会更有意义。例如:

try {
    for(int i = 0; i < max; i++) {
        String myString = ...;
        float myNum = Float.parseFloat(myString);
        dbConnection.update("MY_FLOATS","INDEX",i,"VALUE",myNum);
    }
} catch (NumberFormatException ex) {
    return null;
} finally {
    dbConnection.release();  // Always release DB connection, even if transaction fails.
}

因为无论是否出现错误,您只需一次释放数据库连接(或选择您喜欢的其他资源类型...)。


1

上面未提及的另一个方面是,每个try-catch都会对堆栈产生一定的影响,这可能对递归方法有影响。

如果方法“ outer()”调用方法“ inner()”(可能会递归调用自身),请尝试在方法“ outer()”中找到try-catch。当在内部方法中使用try-catch时,在性能类中使用的一个简单的“堆栈崩溃”示例失败于约6,400帧,而在外部方法中则失败于约11,600帧。

在现实世界中,如果您使用的是Composite模式并且具有大型,复杂的嵌套结构,则可能会遇到问题。


0

如果要为每次迭代捕获Exception,或者检查抛出什么迭代异常并以迭代方式捕获每个Exception,请将try ... catch放入循环内。如果发生Exception,这不会中断循环,您可以在整个循环的每次迭代中捕获每个Exception。

如果要中断循环并在抛出异常时检查异常,请使用try ... catch退出循环。这将中断循环并在catch之后执行语句(如果有)。

这完全取决于您的需要。我更喜欢在部署时在循环内使用try ... catch,因为如果发生Exception,则结果不会模糊,并且循环不会中断并完全执行。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.