应该将try...catch放在循环内部还是外部?

211

我有一个循环,大致如下:

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
我不知道性能如何,但将 try catch 放在 for 循环外面可以使代码看起来更加整洁。 - Jack B Nimble
21个回答

146

性能:

无论 try/catch 结构放在何处,都不会有任何性能差异。在内部,它们被实现为一个代码范围表格,存储在调用该方法时创建的结构中。在方法执行时,除非发生异常,否则 try/catch 结构完全不会产生影响;一旦抛出异常,则会将错误位置与表格进行比较。

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

该表格在一半的位置进行了描述。


好的,你的意思是除了美学上的区别之外没有其他区别。你能提供一个链接作为参考吗? - Michael Myers
2
谢谢。我想自1997年以来情况可能已经发生了变化,但它们几乎不会使其效率降低。 - Michael Myers
1
绝对是一个很棘手的词。在某些情况下,它可能会抑制编译器的优化。虽然不是直接的代价,但它可能会带来间接的性能损失。 - Tom Hawtin - tackline
2
是的,我想我应该稍微控制一下热情,但错误信息让我很恼火!在优化方面,由于我们无法知道性能会受到哪种影响,所以我想我们回到了尝试和测试(一如既往)。 - Jeffrey L Whitledge
1
只要代码没有异常,就不会有性能差异。 - Onur Bıyık
链接已损坏。 - Javad Alimohammadi

80

性能: 正如 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;
    }
}

2
看得更仔细一些。他在两个例子中都是在第一个异常出现时退出的。我一开始也有同样的想法。 - kervin
2
我知道,这就是为什么我说在他的情况下,因为当他遇到问题时返回,那么我会使用外部捕获。为了清晰起见,这是我要使用的规则... - Ray Hayes
好的代码,但不幸的是Java没有输出参数的奢侈。 - Michael Myers
改用 Float 类型,它可以是 null 或者你想要的浮点数值! - Ray Hayes
2
有人建议使用C#/.net中的TryParse,它允许您进行安全解析而无需处理异常,现在已经复制并且该方法是可重用的。至于原始问题,我更喜欢将循环放在catch内部,即使没有性能问题,它看起来更清晰。 - Ray Hayes
显示剩余3条评论

49

好的,根据Jeffrey L Whitledge的说法(截至1997年),在循环中使用或不使用try-catch并没有性能上的区别。为了验证这一点,我进行了如下小规模测试:

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 client VM上绝对没有性能差异(我无法访问其他版本)。整个测试过程中总时间差异只有几毫秒左右。

因此,唯一需要考虑的就是哪种方式看起来更清晰。我发现第二种方式很丑陋,所以我要坚持使用第一种方式或Ray Hayes的方式


如果异常处理程序导致循环流程之外,我认为将其放置在循环之外是最清晰的做法——而不是将catch子句显然放置在循环内部,但仍然返回。对于此类“catch-all”方法的“caught”体,我使用一行空格来更清晰地分离主体和总包含try块。 - Thomas W

18

虽然性能可能相同,而且“外观”更好很主观,但功能上还是存在相当大的差异。看下面这个例子:

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

另一个输出(在 while 循环中使用 try/catch):

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

这里有一个相当重要的区别:

在try/catch中使用break会跳出循环

而在while中使用try/catch会保持循环活跃


1
如果将循环视为单个“单元”,并且在该单元中发现问题会导致整个“单元”(或在本例中的循环)出现问题,则应在循环周围放置try{} catch() {}。如果您将循环的单个迭代或数组中的单个元素视为整个“单元”,则应在循环内部放置try{} catch() {}。如果在该“单元”中发生异常,我们只需跳过该元素,然后继续进行下一个循环迭代。 - Galaxy
从技术上讲是正确的,但他正在返回他的捕获物,所以在这种情况下功能上没有任何区别。 - undefined

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,一般来说我不喜欢有多个返回值,但你懂的。
另一方面,如果你想让它忽略问题,解析它能够解析的任何字符串,你可以将 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
这就是为什么我说:“如果你只想捕获一个错误的值但继续处理,把它放在内部。” - Ray Hayes

5

如前所述,性能是相同的。 然而,用户体验不一定相同。 在第一种情况下,您会快速失败(即在第一个错误后),但是如果您将try/catch块放在循环内部,您可以捕获对于给定方法调用而创建的所有错误。 当从字符串中解析值数组时,如果您希望能够呈现所有格式错误以便用户无需逐个修复它们,则绝对有这样的情况。


这对于这个特定情况不正确(我在catch块中返回),但总体来说,这是一件值得记住的好事。 - Michael Myers

4
只要您知道循环中需要完成的任务,就可以将 try catch 放在循环外部。但重要的是要理解,在异常发生时,循环将立即结束,这可能并不总是您想要的。这实际上是基于 Java 的软件中非常常见的错误。人们需要处理多个项目,例如清空队列,并错误地依赖外部 try/catch 语句来处理所有可能的异常。他们还可能仅在循环内处理特定异常,而不期望发生任何其他异常。如果在循环内部发生未处理的异常,则该循环将被“抢占”,可能会过早地结束并由外部 catch 语句处理异常。
如果循环的任务是清空队列,那么该循环很可能在队列真正清空之前就结束了。非常常见的故障。

4

如果失败是全面的,那么第一种格式就有意义了。如果你想要处理/返回所有非失败元素,你需要使用第二种形式。这些将是我选择方法的基本标准。个人而言,如果是全面性的,我不会使用第二种形式。


3

我认为try/catch块是确保正确异常处理必需的,但创建这样的块会影响性能。由于循环包含密集的重复计算,因此不建议在循环内部放置try/catch块。此外,似乎在发生此条件的情况下,通常捕获的是“Exception”或“RuntimeException”。应避免在代码中捕获RuntimeException。同样,如果您在一家大公司工作,正确记录异常或阻止运行时异常发生是至关重要的。整个描述的重点是请勿在循环中使用try-catch块


是的,这是最好的通用答案,完全避免在循环中使用try/catch可以通过质量工具进行检查,并确保您永远不会为每个迭代创建一个异常(这是一个很大的性能问题)。 - Tristan

2

在您的例子中,两种写法没有功能上的差异。我觉得您的第一个例子更易读。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接