应该尝试...在循环内还是循环外捕获?

时间:2020-03-06 14:48:15  来源:igfitidea点击:

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

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中(可能在其自己的方法中)更干净。但是,仍然存在关于哪个更快的争论。有人可以测试一下并返回统一答案吗?

解决方案

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

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

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

另一方面,我们可能应该看一下float.TryParse或者Convert.ToFloat。

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

每次调用Try / Catch结构时,都会增加方法执行的开销。处理该结构只需要一点内存和处理器的滴答声。如果我们运行了100次循环,并且出于假设的原因,假设每次尝试/捕获调用的成本为1滴答,那么在循环内使用"尝试/捕获"则要花费100滴答,而如果是,则仅需要1滴答在循环之外。

如果将try / catch放入循环内,则在发生异常后仍将继续循环。如果将其置于循环之外,则一旦引发异常,就会立即停止。

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

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

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

但是,我同意其他人的观点,即在循环之外使循环体看起来更干净。

如果我们有可能要继续处理而不是在有无效数字的情况下退出,那么我们将希望代码在循环内。

表现:

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

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

该表大约在中途被描述。

异常的全部目的是鼓励采用第一种方式:让错误处理得以合并和处理一次,而不是立即在每个可能的错误站点进行处理。

好的,在Jeffrey L Whitledge说没有性能差异(截至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优化,Jeffrey是正确的。 Java 6,Sun客户端VM上绝对没有性能差异(我无法访问其他版本)。在整个测试中,总时间差约为几毫秒。

因此,唯一的考虑就是看起来最干净的东西。我发现第二种方法很难看,所以我会坚持第一种方法或者Ray Hayes的方法。

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

请考虑以下示例:

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

  • " try-catch"块的责任"更广泛"(即,在情况下不在循环中)意味着在以后更改代码时,我们可能会错误地添加由现有的catch块处理的行;可能是无意的。就我们而言,这是不太可能的,因为我们显式地捕获了NumberFormatException
  • " try-catch"块的职责"越窄",重构就越困难。特别是(如情况),当我们从catch块(return null语句)中执行"非本地"指令时。

这取决于故障处理。如果我们只想跳过错误元素,请尝试以下操作:

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时,在性能类中使用的一个简单的"堆栈崩溃"示例失败于大约6,400帧,而在外部方法中则失败于大约11,600帧。

在现实世界中,如果我们使用的是Composite模式并且具有大型,复杂的嵌套结构,则可能会遇到问题。