关于java:应该尝试…在循环内部或外部捕获?

Should try…catch go inside or outside a loop?

我有一个这样的循环:

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

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

1
2
3
4
5
6
7
8
9
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块放在循环中,如下所示:

1
2
3
4
5
6
7
8
9
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中比较干净,可能放在自己的方法中。然而,关于哪种速度更快仍然存在争议。有人能测试一下这个并给出一个统一的答案吗?


性能:

在放置Try/Catch结构的位置上,绝对没有性能差异。在内部,它们在调用方法时创建的结构中作为代码范围表实现。当该方法正在执行时,除非发生抛出,否则Try/Catch结构完全不在图片中,然后将错误的位置与表进行比较。

参考文献:http://www.javaworld.com/javaworld/jw-01-1997/jw-01-hood.html

这张桌子被描述为大约下了一半。


性能:正如杰夫瑞在回答中所说的,在Java中,它没有多大区别。

通常,为了代码的可读性,您选择在哪里捕获异常取决于您是否希望循环保持处理。

在您的示例中,捕获异常时返回。在这种情况下,我会把尝试/捕获放在循环中。如果你只是想捕捉一个错误的值,但要继续处理,就把它放进去。

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

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


好吧,在Jeffrey L Whitledge说没有性能差异之后(1997年),我去测试了它。我做了一个小基准:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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优化不重要,杰夫瑞是正确的;在Java 6,Sun Client VM(我没有访问其他版本)上绝对没有性能差异。在整个测试过程中,总时差大约为几毫秒。

因此,唯一要考虑的是什么看起来最干净。我发现第二条路很丑,所以我要么坚持第一条路,要么坚持雷·海斯的路。


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

考虑一下这个稍微修改过的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
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()方法在出现任何错误(如原始示例)时返回空值,您可以将try/catch放在外部,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
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;
}

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

另一方面,如果您希望它忽略问题,并解析它可以解析的任何字符串,您可以将try/catch放在循环的内部,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}


虽然性能可能是相同的,而"看起来"更好的是非常主观的,但功能上仍然存在很大的差异。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
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时抛出异常。

在任何细节之前,这里还有另一个例子:

1
2
3
4
5
6
7
8
9
10
11
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中):

1
2
3
4
5
4
8
12
16
in catch block

另一个输出(Try/Catch-In-While):

1
2
3
4
5
6
7
8
9
10
4
8
12
16
in catch block
24
28
32
36
40

在这里,你有一个非常显著的区别:

当in try/catch从循环中断时

在保持循环活动的同时尝试/捕获


如前所述,性能是相同的。然而,用户体验并不一定相同。在第一种情况下,您将很快失败(即在第一个错误之后),但是如果将try/catch块放入循环中,您可以捕获为对该方法的给定调用创建的所有错误。在分析字符串中的值数组时,如果您希望出现一些格式错误,那么在某些情况下,您肯定希望能够向用户显示所有错误,这样他们就不需要逐个尝试修复这些错误。


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


只要您知道在循环中需要完成什么,就可以将try-catch放到循环之外。但重要的是要理解,一旦异常发生,循环就会结束,这可能并不总是您想要的。这实际上是基于Java的软件中的一个非常常见的错误。人们需要处理许多项目,例如清空队列,并错误地依赖外部try/catch语句来处理所有可能的异常。它们还可以只处理循环内的特定异常,而不期望发生任何其他异常。然后,如果发生了一个未在循环内处理的异常,那么循环将被"抢占",它可能过早结束,并且外部catch语句处理该异常。

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


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

在另一个注释中,您可能会看到float.typarse或convert.tofloat。


如果将try/catch放在循环中,则会在发生异常后继续循环。如果把它放在循环之外,一旦抛出异常,就会立即停止。


在您的示例中,没有功能差异。我发现你的第一个例子更易读。


我的观点是,尝试/捕获块对于确保正确的异常处理是必要的,但是创建这样的块会影响性能。由于循环包含大量重复计算,因此不建议将try/catch块放在循环中。此外,在这种情况出现的地方,通常会捕获"exception"或"runtimeexception"。应避免在代码中捕获RuntimeException。同样,如果您在一家大公司工作,那么必须正确记录该异常,或者停止运行时异常的发生。描述的重点是PLEASE AVOID USING TRY-CATCH BLOCKS IN LOOPS


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

如果方法"outer()"调用方法"inner()"(它可能递归地调用自己),请尽可能在方法"outer()"中查找try catch。我们在性能类中使用的一个简单的"堆栈崩溃"示例在try catch位于内部方法时在6400帧处失败,在外部方法时在11600帧处失败。

在现实世界中,如果您使用复合模式并且具有大型、复杂的嵌套结构,这可能是一个问题。


把它放进去。您可以继续处理(如果需要),也可以抛出一个有用的异常,告诉客户mystring的值和包含错误值的数组的索引。我认为NumberFormatException已经告诉了你错误的值,但原则是将所有有用的数据放入你抛出的异常中。考虑一下在这个程序的调试程序中,您将感兴趣的是什么。

考虑:

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

在需要的时候,你会非常感激这样一个例外,其中包含尽可能多的信息。


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

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

不过,我同意其他人的观点,把它放在圈外会使圈体看起来更干净。

如果有可能您希望继续处理,而不是退出(如果有无效的数字),那么您希望代码在循环中。


我想补充一下我自己的0.02c,在研究在哪里定位异常处理的一般问题时,有两个相互竞争的考虑:

  • try-catch块的"更广泛"责任(即,在您的情况下,在循环之外)意味着,在以后更改代码时,您可能会错误地添加一条由现有catch块处理的行;可能是无意中添加的。在您的情况下,这是不太可能的,因为您正在明确地捕获一个NumberFormatException

  • try-catch块的职责越"狭窄",重构就越困难。尤其是当(在您的例子中)您从catch块(return null语句)中执行"非本地"指令时。


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    for(int i = 0; i < max; i++) {
        String myString = ...;
        try {
            float myNum = Float.parseFloat(myString);
            myFloats[i] = myNum;
        } catch (NumberFormatException ex) {
            --i;
        }
    }

    在其他情况下,我更喜欢在外面试试。代码更可读,更干净。如果返回null,那么最好在错误案例中抛出IllegalArgumentException。


    如果它在内部,那么您将获得n次Try/Catch结构的开销,而不仅仅是外部的一次。

    每次调用try/catch结构时,都会增加方法执行的开销。只是处理结构所需的一点点内存和处理器的滴答声。如果一个循环运行了100次,并且出于假设的原因,假设每次尝试/捕获调用的成本是1个勾号,那么在循环内尝试/捕获将花费100个勾号,而在循环外仅花费1个勾号。


    我把0.02美元放进去。有时,您最终需要在代码中添加一个"finally"(最后一次),因为谁第一次完美地编写了他们的代码?。在这些情况下,突然间,在循环之外进行尝试/捕获更有意义。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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.
    }

    因为如果你得到一个错误,或者没有,你只想释放你的数据库连接(或者选择你最喜欢的其他资源类型…)。


    例外的全部要点是鼓励第一种风格:让错误处理合并并处理一次,而不是在每个可能的错误站点立即处理。


    如果您想要捕捉每个迭代的异常,或者检查抛出了什么迭代异常并捕捉一个itertation中的每个异常,请将try…catch放入循环中。如果发生异常,这不会中断循环,并且您可以在整个循环中捕获每个迭代中的每个异常。

    如果要中断循环并在每次抛出时检查异常,请使用try…catch out of the loop。这将中断循环,并在catch(如果有)之后执行语句。

    这完全取决于你的需要。我更喜欢在部署as时在循环内使用try…catch,如果发生异常,结果不会模棱两可,循环也不会完全中断和执行。