应该尝试......赶上环路内外?

时间:2008-09-26 19:52:30

标签: java performance loops try-catch

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

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中可能更清晰,可能在其自己的方法中。但是,关于哪个更快,仍然存在争议。有人可以对此进行测试,并以统一的答案回来吗?

21 个答案:

答案 0 :(得分:122)

性能:

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

以下是参考资料:http://www.javaworld.com/javaworld/jw-01-1997/jw-01-hood.html

该表描述了一半。

答案 1 :(得分:68)

效果:正如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 :(得分:46)

好吧,在Jeffrey L Whitledge said之后没有性能差异(截至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优化不明显,杰弗里是正确的; Java 6,Sun客户端虚拟机绝对没有性能差异(我没有访问其他版本)。在整个测试中,总时间差大约为几毫秒。

因此,唯一的考虑因素是看起来最干净。我发现第二种方式很难看,所以我会坚持第一种方式或Ray Hayes's way

答案 3 :(得分: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()方法在有任何错误时返回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;
}

答案 4 :(得分:13)

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

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中突破循环

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

答案 5 :(得分:5)

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

答案 6 :(得分:4)

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

答案 7 :(得分:4)

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

如果循环在生命中扮演了清空队列的角色,那么该循环很可能在该队列被清空之前结束。很常见的错误。

答案 8 :(得分:2)

如果将try / catch放在循环中,则会在异常后继续循环。如果你把它放在循环外面,你会在抛出异常时立即停止。

答案 9 :(得分:2)

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

答案 10 :(得分:2)

您应该更喜欢外部版本而不是内部版本。这只是规则的特定版本,移动循环外的任何东西,你可以移动到循环外。根据IL编译器和JIT编译器,您的两个版本可能会或可能不会以不同的性能特征结束。

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

答案 11 :(得分:2)

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

答案 12 :(得分:1)

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

取决于迭代次数,性能差异可能微不足道。

但是我同意其他人认为在循环之外使循环体看起来更清洁。

如果有可能你想继续处理而不是退出,如果有一个无效的数字,那么你会希望代码在循环内。

答案 13 :(得分:1)

异常的全部意义在于鼓励第一种方式:让错误处理得到整合和处理一次,而不是立即在每个可能的错误站点处进行。

答案 14 :(得分:1)

如果它在里面,那么你将获得尝试/捕获结构的开销N次,而不是只在外面的一次。


每次调用Try / Catch结构时,都会增加方法执行的开销。只是一点点记忆和处理结构所需的处理器滴答。如果你正在运行一个循环100次,并且为了假设的缘故,假设每次尝试/捕获调用的成本是1个滴答,那么在循环内部使用Try / Catch会花费100个滴答,而不是只有1个滴答,如果它是在循环之外。

答案 15 :(得分:1)

把它放进去。您可以继续处理(如果需要),也可以抛出一个有用的异常,告诉客户端myString的值和包含错误值的数组的索引。我认为NumberFormatException已经告诉你错误的值,但原则是将所有有用的数据放在你抛出的异常中。想想在程序中此时调试器中你会感兴趣的内容。

考虑一下:

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

在需要的时候,你会非常感谢这样的例外,尽可能多地提供信息。

答案 16 :(得分:1)

在查看异常处理位置的一般问题时,我想添加自己的0.02c两个相互竞争的考虑因素:

  1. try-catch块的“更广泛”的责任(即在你的情况下在循环之外)意味着在稍后更改代码时,你可能会错误地添加一个由您现有的catch区块;可能是无意的。在您的情况下,这不太可能,因为您明确捕获NumberFormatException

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

答案 17 :(得分:1)

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

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

在任何其他情况下,我更喜欢在外面尝试。代码更易读,更干净。也许最好在错误情况下抛出IllegalArgumentException而不是返回null。

答案 18 :(得分: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.
}

因为如果您收到错误,您只想发布一次数据库连接(或选择您喜欢的其他资源类型......)。

答案 19 :(得分:1)

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

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

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

答案 20 :(得分:0)

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

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

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