应该……接住环内还是环外?

我有一个这样的循环:

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中更干净,可能是在它自己的方法中。然而,关于哪个速度更快仍存在争议。有人能测试一下并给出一个统一的答案吗?

137923 次浏览

在你的例子中没有功能上的区别。我觉得你的第一个例子可读性更强。

性能:正如杰弗里在他的回复中所说,在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;
}
}

你应该喜欢外面的版本而不是里面的版本。这只是规则的一个特定版本,将任何可以移动到循环外的东西移动到循环外。根据IL编译器和JIT编译器的不同,您的两个版本最终可能具有不同的性能特征,也可能没有。

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

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


每次调用Try/Catch结构都会增加方法执行的开销。只是一点点记忆&处理结构所需的处理器节拍。如果运行一个循环100次,假设每个try/catch调用的代价是1 tick,那么在循环内执行try/catch调用的代价是100 tick,而在循环外只执行1 tick。

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

如果这是一个全有或全无的失败,那么第一种格式是有意义的。如果希望能够处理/返回所有未失败的元素,则需要使用第二种形式。这些就是我选择方法的基本标准。就我个人而言,如果是孤注一掷,我不会使用第二种形式。

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

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

然而,我同意其他人的观点,把它放在循环之外会让循环体看起来更干净。

如果您希望继续处理,而不是在存在无效数字时退出,那么您将希望代码位于循环中。

性能:

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

这里是一个引用:http://www.javaworld.com/javaworld/jw-01-1997/jw-01-hood.html

这张桌子在一半的地方被描述。

异常的全部意义在于鼓励第一种风格:让错误处理被合并并处理一次,而不是在每个可能的错误位置立即处理。

好吧,在杰弗里·L·惠特利奇说之后,没有性能差异(截至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客户端VM上没有性能差异(我没有访问其他版本)。整个测试的总时间差在几毫秒的数量级上。

因此,唯一要考虑的是什么看起来最干净。我发现第二种方式是丑陋的,所以我将坚持第一种方式或雷·海耶斯的方式

我同意所有关于性能和可读性的帖子。然而,在某些情况下,这确实很重要。其他一些人也提到了这一点,但通过例子可能更容易理解。

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

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;
}

把它放在里面。您可以继续处理(如果您愿意),也可以抛出一个有用的异常,告诉客户端myString的值和包含坏值的数组的索引。我认为NumberFormatException已经告诉您坏的值,但原则是将所有有用的数据放在您抛出的异常中。考虑一下在程序的调试器中,您会对什么感兴趣。

考虑:

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

在需要的时候,您将非常感谢这样一个异常,其中包含尽可能多的信息。

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

  1. try-catch块的“更宽”职责(即在你的情况下在循环之外)意味着在稍后更改代码时,你可能会错误地添加由你现有的catch块处理的一行;可能无意中。在你的情况下,这是不太可能的,因为你显式地捕捉NumberFormatException

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

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

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

在任何其他情况下,我宁愿到外面去试试。代码更可读,更干净。如果返回null,在错误情况下抛出一个IllegalArgumentException可能会更好。

我把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.
}

因为如果出现错误,您只希望释放一次数据库连接(或选择您最喜欢的其他资源类型……)。

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

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

如果方法"outer()"调用方法"inner()"(它可能递归地调用自己),如果可能的话,尝试在方法"outer()"中找到try-catch。我们在性能类中使用的一个简单的“堆栈崩溃”示例,当try-catch在内部方法中时,在大约6400帧时失败,当它在外部方法中时,在大约11,600帧时失败。

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

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

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

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

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

这里有很大的不同:

While in try/catch跳出循环

Try /catch in while保持循环活动

如果你想在每个迭代中捕获Exception,或者检查哪个迭代抛出了Exception,并捕获迭代中的每个Exception,放置try…在循环内捕获。如果发生异常,这将不会中断循环,并且您可以在整个循环的每次迭代中捕获每个异常。

如果你想打破循环并在抛出异常时检查异常,请使用try…从循环中捕获。这将打破循环并在catch(如果有的话)之后执行语句。

这完全取决于你的需要。我更喜欢用try…如果Exception发生,结果不会有歧义,并且循环不会中断并完全执行。

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