it-swarm.com.de

Sollte es versuchen ... fangen Sie innerhalb oder außerhalb einer Schleife?

Ich habe eine Schleife, die ungefähr so ​​aussieht:

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

Dies ist der Hauptinhalt einer Methode, deren einziger Zweck darin besteht, das Array von Floats zurückzugeben. Ich möchte, dass diese Methode null zurückgibt, wenn ein Fehler auftritt. Daher füge ich die Schleife in einen try...catch-Block ein:

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

Aber dann dachte ich auch daran, den try...catch-Block innerhalb der Schleife zu platzieren:

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

Gibt es einen Grund, eine Leistung oder sonstwie, eine der anderen vorzuziehen?


Edit: Der Konsens scheint zu sein, dass es sauberer ist, die Schleife in Try/Catch zu setzen, möglicherweise in eine eigene Methode. Es gibt jedoch noch Diskussionen darüber, welche schneller sind. Kann jemand das testen und mit einer einheitlichen Antwort zurückkommen?

168
Michael Myers

In Ordnung, nachdem Jeffrey L. Whitledge sagte dass es keinen Leistungsunterschied gab (Stand 1997), habe ich es getestet. Ich lief diesen kleinen Benchmark:

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

Ich habe den resultierenden Bytecode mit javap überprüft, um sicherzustellen, dass nichts eingebettet wurde.

Die Ergebnisse zeigten, dass Jeffrey ist richtig; Es gibt absolut kein Leistungsunterschied auf Java 6, Sun-Client VM) (Ich hatte keinen Zugriff auf andere Versionen). Die Gesamtzeitdifferenz liegt im gesamten Test in der Größenordnung von wenigen Millisekunden.

Daher ist die einzige Überlegung, was am saubersten aussieht. Ich finde, dass der zweite Weg hässlich ist, also bleibe ich entweder beim ersten Weg oder bei Ray Hayes .

46
Michael Myers

PERFORMANCE:

Es gibt absolut keinen Leistungsunterschied hinsichtlich der Platzierung der Try/Catch-Strukturen. Intern werden sie als Codebereichstabelle in einer Struktur implementiert, die beim Aufruf der Methode erstellt wird. Während die Methode ausgeführt wird, befinden sich die try/catch-Strukturen vollständig außerhalb des Bildes, es sei denn, ein Wurf erfolgt. Dann wird die Position des Fehlers mit der Tabelle verglichen.

Hier ist eine Referenz: http://www.javaworld.com/javaworld/jw-01-1997/jw-01-hood.html

Die Tabelle wird ungefähr zur Hälfte beschrieben.

120

Performance: Wie Jeffrey in seiner Antwort sagte, in Java macht es keinen großen Unterschied.

Im Allgemeinen, zur besseren Lesbarkeit des Codes hängt Ihre Entscheidung, wo die Ausnahme abgefangen werden soll, davon ab, ob die Schleife weiter verarbeitet werden soll oder nicht. 

In Ihrem Beispiel sind Sie nach dem Erkennen einer Ausnahme zurückgekehrt. In diesem Fall würde ich den Try/Catch um die Schleife legen. Wenn Sie nur einen schlechten Wert erhalten möchten, aber die Verarbeitung fortsetzen möchten, geben Sie ihn ein. 

Der dritte Weg: Sie könnten immer Ihre eigene statische ParseFloat-Methode schreiben und die Ausnahmebehandlung in dieser Methode und nicht in Ihrer Schleife behandeln. Die Ausnahmebehandlung wird für die Schleife selbst isoliert!

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;
    }
}
68
Ray Hayes

Während die Leistung möglicherweise gleich ist und das, was "besser" aussieht, sehr subjektiv ist, besteht immer noch ein ziemlich großer Unterschied in der Funktionalität. Nehmen Sie das folgende Beispiel:

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

Die while-Schleife befindet sich innerhalb des try-catch-Blocks, die Variable 'j' wird inkrementiert, bis sie auf 40 trifft und ausgedruckt wird, wenn j mod 4 Null ist und eine Ausnahme ausgelöst wird, wenn j 20 erreicht.

Vor allen Details hier das andere Beispiel:

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

Dieselbe Logik wie oben, der einzige Unterschied besteht darin, dass sich der try/catch-Block jetzt in der while-Schleife befindet. 

Hier kommt die Ausgabe (während in try/catch):

4
8
12 
16
in catch block

Und die andere Ausgabe (try/catch in while):

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

Da gibt es einen erheblichen Unterschied: 

während in try/catch aus der Schleife ausbricht

try/catch in während die Schleife aktiv bleibt

13
seBaka28

Ich stimme allen Leistungs- und Lesbarkeitsbeiträgen zu. Es gibt jedoch Fälle, in denen es wirklich wichtig ist. Ein paar andere Leute haben dies erwähnt, aber es könnte einfacher sein, Beispiele zu sehen.

Betrachten Sie dieses leicht modifizierte Beispiel:

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

Wenn Sie möchten, dass die parseAll () - Methode bei Fehlern (wie im ursprünglichen Beispiel) null zurückgibt, würden Sie try/catch so auf die Außenseite setzen:

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

In der Realität sollten Sie hier wahrscheinlich einen Fehler anstelle von Null zurückgeben, und im Allgemeinen mag ich nicht mehrere Rückgaben, aber Sie haben die Idee.

Auf der anderen Seite, wenn Sie möchten, dass die Probleme einfach ignoriert werden und die möglichen Strings analysiert werden, würden Sie den try/catch so in das Innere der Schleife einfügen:

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;
}
13
Matt N

Wie bereits erwähnt, ist die Leistung gleich. Die Benutzererfahrung muss jedoch nicht unbedingt identisch sein. Im ersten Fall schlagen Sie schnell aus (d. H. Nach dem ersten Fehler). Wenn Sie jedoch den try/catch-Block in die Schleife einfügen, können Sie alle Fehler erfassen, die für einen bestimmten Aufruf der Methode erstellt würden. Wenn Sie ein Array von Werten aus Zeichenfolgen analysieren, bei denen Sie Formatierungsfehler erwarten, gibt es auf jeden Fall Fälle, in denen Sie alle Fehler an den Benutzer weitergeben möchten, damit er sie nicht nacheinander reparieren und beheben muss .

5
user19810

Wenn ein Alles-oder-Nichts-Fehler auftritt, ist das erste Format sinnvoll. Wenn Sie alle fehlerfreien Elemente verarbeiten/zurückgeben möchten, müssen Sie das zweite Formular verwenden. Das wären meine Grundkriterien für die Wahl zwischen den Methoden. Wenn es alles oder nichts ist, würde ich das zweite Formular nicht verwenden.

4
Joe Skora

Solange Sie wissen, was Sie in der Schleife erreichen müssen, können Sie den Try-Catch außerhalb der Schleife platzieren. Es ist jedoch wichtig zu verstehen, dass die Schleife dann endet, sobald die Ausnahme auftritt und dies möglicherweise nicht immer das ist, was Sie möchten. Dies ist tatsächlich ein sehr häufiger Fehler in Java-basierter Software. Benutzer müssen eine Reihe von Elementen verarbeiten, beispielsweise das Leeren einer Warteschlange, und sie verlassen sich fälschlicherweise auf eine äußere Try/Catch-Anweisung, die alle möglichen Ausnahmen behandelt. Sie könnten auch nur eine bestimmte Ausnahme innerhalb der Schleife behandeln und erwarten nicht, dass eine andere Ausnahme auftritt .. Wenn dann eine Ausnahme auftritt, die nicht innerhalb der Schleife behandelt wird, wird die Schleife "preemted", sie endet möglicherweise vorzeitig und Die äußere catch-Anweisung behandelt die Ausnahme.

Wenn die Schleife eine Rolle im Leben zum Leeren einer Warteschlange hatte, könnte diese Schleife sehr wahrscheinlich enden, bevor diese Warteschlange wirklich geleert wurde. Sehr häufiger Fehler.

In Ihren Beispielen gibt es keinen funktionalen Unterschied. Ich finde dein erstes Beispiel lesbarer.

2
Jamie

Sie sollten die äußere Version der inneren Version vorziehen. Dies ist nur eine bestimmte Version der Regel. Bewegen Sie alles außerhalb der Schleife, das Sie außerhalb der Schleife verschieben können. Abhängig von dem IL-Compiler und dem JIT-Compiler haben Ihre beiden Versionen möglicherweise unterschiedliche Leistungsmerkmale.

An anderer Stelle sollten Sie sich wahrscheinlich Float.TryParse oder Convert.ToFloat ansehen.

2
Orion Adrian

Wenn Sie den try/catch in die Schleife einfügen, werden Sie nach einer Ausnahme weiterlaufen. Wenn Sie es außerhalb der Schleife platzieren, halten Sie an, sobald eine Ausnahme ausgelöst wird.

2
Joel Coehoorn

Aus meiner Sicht wären try/catch-Blöcke notwendig, um eine korrekte Ausnahmebehandlung zu gewährleisten, aber das Erstellen solcher Blöcke hat Auswirkungen auf die Leistung. Da Loops intensive wiederholte Berechnungen enthalten, wird empfohlen, try/catch-Blöcke nicht in Loops zu setzen. Außerdem scheint es, wo diese Bedingung auftritt. Oft werden "Exception" oder "RuntimeException" abgefangen. RuntimeException, die im Code abgefangen wird, sollte vermieden werden. Auch wenn Sie in einem großen Unternehmen arbeiten, ist es wichtig, diese Ausnahme ordnungsgemäß zu protokollieren oder die Laufzeitausnahme zu stoppen. Der gesamte Punkt dieser Beschreibung ist PLEASE AVOID USING TRY-CATCH BLOCKS IN LOOPS

1
surendrapanday

Wenn es drin ist, gewinnen Sie den Aufwand für die try/catch-Struktur N-mal, im Gegensatz zu nur einmal außerhalb.


Jedes Mal, wenn eine Try/Catch-Struktur aufgerufen wird, erhöht dies die Ausführung der Methode. Nur ein bisschen Speicher und Prozessor-Ticks, die benötigt werden, um mit der Struktur umzugehen. Wenn Sie eine Schleife 100-mal ausführen, und aus hypothetischen Gründen, lassen Sie uns sagen, die Kosten betragen 1 Tick pro Try/Catch-Aufruf. Wenn Sie Try/Catch innerhalb der Schleife haben, kostet Sie 100 Ticks, im Gegensatz zu nur 1 Tick außerhalb der Schleife.

1

das Einrichten eines speziellen Stack-Frames für Try/Catch fügt zusätzlichen Aufwand hinzu. Die JVM kann jedoch möglicherweise die Tatsache erkennen, dass Sie zurückkehren, und diese Optimierung optimieren.

abhängig von der Anzahl der Iterationen ist der Leistungsunterschied wahrscheinlich vernachlässigbar.

Ich stimme jedoch mit den anderen überein, dass es außerhalb der Schleife liegt, dass der Schleifenkörper sauberer aussieht.

Wenn die Chance besteht, dass Sie mit der Verarbeitung fortfahren möchten, anstatt bei einer ungültigen Nummer den Vorgang zu beenden, möchten Sie, dass sich der Code in der Schleife befindet.

1
Matt

Die einzige Ausnahme ist, den ersten Stil zu fördern: Die Fehlerbehandlung einmal konsolidieren und behandeln lassen, nicht sofort an jeder möglichen Fehlerstelle.

1
wnoise

Ich möchte gerne meinen eigenen 0.02c zu zwei konkurrierenden Überlegungen hinzufügen, wenn ich das allgemeine Problem der Positionierung der Ausnahmebehandlung betrachten möchte:

  1. Je "breiter" die Verantwortung des try-catch-Blocks (d. H. Außerhalb der Schleife in Ihrem Fall) ist, wenn Sie den Code zu einem späteren Zeitpunkt ändern, können Sie versehentlich eine Zeile hinzufügen, die von Ihrem vorhandenen catch-Block verarbeitet wird. möglicherweise unbeabsichtigt. In Ihrem Fall ist dies weniger wahrscheinlich, da Sie explizit eine NumberFormatException abfangen.

  2. Je "enger" die Verantwortung des try-catch-Blocks ist, desto schwieriger wird das Refactoring. Insbesondere wenn (wie in Ihrem Fall) Sie eine "nicht lokale" Anweisung innerhalb des catch-Blocks ausführen (die return null-Anweisung). 

1
oxbow_lakes

Das hängt von der Fehlerbehandlung ab. Wenn Sie nur die Fehlerelemente überspringen möchten, versuchen Sie Folgendes:

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

In allen anderen Fällen würde ich es lieber draußen probieren. Der Code ist besser lesbar und sauberer. Vielleicht wäre es besser, eine IllegalArgumentException im Fehlerfall zu werfen, wenn Sie null zurückgeben.

1
Arne Burmeister

lege es hinein. Sie können die Verarbeitung fortsetzen (wenn Sie möchten) oder eine hilfreiche Ausnahme auslösen, die dem Client den Wert von myString und den Index des Arrays mit dem ungültigen Wert mitteilt. Ich denke, NumberFormatException wird Ihnen bereits den schlechten Wert mitteilen, aber das Prinzip ist, alle hilfreichen Daten in die Ausnahmen zu setzen, die Sie werfen. Überlegen Sie, was Sie an dieser Stelle im Programm für den Debugger interessieren könnte. 

Erwägen: 

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

In der Zeit der Not werden Sie eine solche Ausnahme mit so vielen Informationen wie möglich zu schätzen wissen.

1
Kyle Dyer

Ich gebe meine $ 0,02 in. Manchmal müssen Sie später in Ihrem Code ein "final" hinzufügen (weil wer den Code beim ersten Mal perfekt schreibt?). In solchen Fällen ist es plötzlich sinnvoller, Try/Catch außerhalb der Schleife zu haben. Zum Beispiel:

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

Wenn Sie einen Fehler erhalten oder nicht, möchten Sie Ihre Datenbankverbindung nur einmal freigeben (oder Ihren bevorzugten Ressourcentyp auswählen ...).

1
Ogre Psalm33

Ein weiterer Aspekt, der oben nicht erwähnt wurde, ist die Tatsache, dass jeder try-catch einige Auswirkungen auf den Stack hat, was Auswirkungen auf rekursive Methoden haben kann.

Wenn die Methode "outer ()" die Methode "inner ()" aufruft (die sich selbst rekursiv nennen kann), versuchen Sie, den try-catch in der Methode "outer ()" zu finden, wenn möglich. Ein einfaches "Stack-Crash" -Beispiel, das wir in einer Leistungsklasse verwenden, versagt bei etwa 6.400 Frames, wenn Try-Catch in der inneren Methode ausgeführt wird, und bei etwa 11.600 Frames, wenn es sich bei der äußeren Methode handelt.

In der realen Welt kann dies ein Problem sein, wenn Sie das Composite-Muster verwenden und große, komplexe verschachtelte Strukturen haben.

1
Jeff Hill

Wenn Sie Exception für jede Iteration abfangen möchten oder prüfen möchten, welche Iteration Exception ausgelöst wird und alle Exceptions in einer Iteration abfangen, setzen Sie try ... catch in die Schleife. Dadurch wird die Schleife nicht unterbrochen, wenn Exception auftritt, und Sie können jede Exception in jeder Iteration in der gesamten Schleife abfangen.

Wenn Sie die Schleife unterbrechen und die Exception bei jedem Wurf untersuchen möchten, verwenden Sie try ... catch aus der Schleife. Dadurch wird die Schleife unterbrochen und Anweisungen nach catch (falls vorhanden) ausgeführt.

Es hängt alles von Ihrem Bedarf ab. Ich bevorzuge die Verwendung von try ... catch innerhalb der Schleife während der Bereitstellung, da bei Exception die Ergebnisse nicht mehrdeutig sind und die Schleife nicht bricht und nicht vollständig ausgeführt wird.

0