Should try…catch go inside or outside a loop?
我有一个这样的循环:
1 2 3 4 5 |
这是方法的主要内容,其唯一目的是返回浮点数组。如果出现错误,我希望此方法返回
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; } |
号
但后来我又想把
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 |
实际上,您可能应该在这里返回一个错误,而不是空值,通常我不喜欢有多个返回,但是您知道了。
另一方面,如果您希望它忽略问题,并解析它可以解析的任何字符串,您可以将try/catch放在循环的内部,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
。
虽然性能可能是相同的,而"看起来"更好的是非常主观的,但功能上仍然存在很大的差异。举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 |
while循环在try-catch块中,变量"j"递增到40,当j mod 4为零时打印出来,当j达到20时抛出异常。
在任何细节之前,这里还有另一个例子:
1 2 3 4 5 6 7 8 9 10 11 |
号
与上面的逻辑相同,唯一的区别是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。同样,如果您在一家大公司工作,那么必须正确记录该异常,或者停止运行时异常的发生。描述的重点是
上面没有提到的另一个方面是,每个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可能能够检测到您正在返回并优化这一事实。
根据迭代次数,性能差异可能可以忽略不计。
不过,我同意其他人的观点,把它放在圈外会使圈体看起来更干净。
如果有可能您希望继续处理,而不是退出(如果有无效的数字),那么您希望代码在循环中。
我想补充一下我自己的
这取决于故障处理。如果您只想跳过错误元素,请在内部尝试:
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,如果发生异常,结果不会模棱两可,循环也不会完全中断和执行。