Dovresti provare ... catturare andare dentro o fuori un ciclo?


185

Ho un ciclo simile a questo:

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

Questo è il contenuto principale di un metodo il cui unico scopo è restituire l'array di float. Voglio che questo metodo ritorni in nullcaso di errore, quindi inserisco il ciclo in un try...catchblocco, in questo modo:

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

Ma poi ho anche pensato di inserire il try...catchblocco all'interno del loop, in questo modo:

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

C'è qualche motivo, prestazione o altro, preferire l'uno all'altro?


Modifica: il consenso sembra essere che sia più pulito inserire il loop all'interno del try / catch, possibilmente all'interno del proprio metodo. Tuttavia, c'è ancora dibattito su quale sia più veloce. Qualcuno può provare questo e tornare con una risposta unificata?


2
Non conosco le prestazioni, ma il codice sembra più pulito con il tentativo catch al di fuori del ciclo for.
Jack B Nimble,

Risposte:


132

PRESTAZIONE:

Non vi è assolutamente alcuna differenza di prestazioni nel punto in cui sono posizionate le strutture try / catch. Internamente, vengono implementati come una tabella di intervalli di codice in una struttura creata quando viene chiamato il metodo. Mentre il metodo è in esecuzione, le strutture try / catch sono completamente fuori dal quadro a meno che non si verifichi un lancio, quindi la posizione dell'errore viene confrontata con la tabella.

Ecco un riferimento: http://www.javaworld.com/javaworld/jw-01-1997/jw-01-hood.html

La tabella è descritta a metà circa.


Ok, quindi stai dicendo che non c'è differenza se non l'estetica. Puoi collegarti a una fonte?
Michael Myers

2
Grazie. Suppongo che le cose potrebbero essere cambiate dal 1997, ma difficilmente le renderebbero meno efficienti.
Michael Myers

1
Assolutamente è una parola difficile. In alcuni casi può inibire le ottimizzazioni del compilatore. Non è un costo diretto, ma può essere una penalità di prestazione indiretta.
Tom Hawtin - tackline

2
È vero, immagino che avrei dovuto regnare un po 'nel mio entusiasmo, ma la disinformazione mi dava sui nervi! Nel caso delle ottimizzazioni, dal momento che non possiamo sapere in che modo le prestazioni verrebbero influenzate, credo che siamo tornati a provare e testare (come sempre).
Jeffrey L Whitledge,

1
non c'è alcuna differenza di prestazioni, solo se il codice viene eseguito senza eccezioni.
Onur Bıyık,

72

Performance : come ha detto Jeffrey nella sua risposta, in Java non fa molta differenza.

In generale , per la leggibilità del codice, la scelta di dove catturare l'eccezione dipende dal fatto che si desideri che l'elaborazione del ciclo continui o meno.

Nel tuo esempio sei tornato a prendere un'eccezione. In tal caso, metterei il try / catch intorno al loop. Se vuoi semplicemente catturare un valore negativo ma continuare l'elaborazione, inseriscilo.

Il terzo modo : puoi sempre scrivere il tuo metodo ParseFloat statico e gestire l'eccezione in quel metodo anziché nel tuo ciclo. Rendere la gestione delle eccezioni isolata al loop stesso!

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

1
Guarda da vicino. Sta uscendo con la prima eccezione in entrambi. All'inizio ho pensato anche la stessa cosa.
Kervin,

2
Ero consapevole, ecco perché ho detto nel suo caso, poiché sta tornando quando incontra un problema, quindi utilizzerei una cattura esterna. Per chiarezza, questa è la regola che userei ...
Ray Hayes,

Bel codice, ma purtroppo Java non ha il lusso di avere parametri.
Michael Myers

ByRef? È passato tanto tempo da quando ho toccato Java!
Ray Hayes,

2
Qualcun altro ha suggerito TryParse che in C # /. Net consente di eseguire un'analisi sicura senza occuparsi delle eccezioni, che ora è replicato e il metodo è riutilizzabile. Per quanto riguarda la domanda originale, preferirei il loop all'interno del fermo, sembra solo più pulito anche se non ci sono problemi di prestazioni ..
Ray Hayes,

46

Va bene, dopo che Jeffrey L Whitledge ha dichiarato che non c'erano differenze di prestazioni (a partire dal 1997), sono andato a testarlo. Ho eseguito questo piccolo 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;
        }
    }
}

Ho controllato il bytecode risultante usando javap per assicurarmi che nulla fosse allineato.

I risultati hanno mostrato che, assumendo insignificanti ottimizzazioni JIT, Jeffrey ha ragione ; non c'è assolutamente alcuna differenza di prestazioni su Java 6, VM client Sun (non avevo accesso ad altre versioni). La differenza di tempo totale è dell'ordine di pochi millisecondi durante l'intero test.

Pertanto, l'unica considerazione è ciò che sembra più pulito. Trovo che la seconda strada sia brutta, quindi mi atterrò alla prima o alla via di Ray Hayes .


Se il gestore delle eccezioni prende il flusso al di fuori del ciclo, allora sento che è più chiaro posizionarlo al di fuori del ciclo - Piuttosto che posizionare una clausola catch apparentemente all'interno del ciclo, che tuttavia ritorna. Uso una linea di spazi bianchi attorno al corpo "catturato" di tali metodi "catch-all", per separare più chiaramente il corpo dal blocco try tutto racchiuso .
Thomas W,

16

Mentre le prestazioni potrebbero essere le stesse e ciò che "sembra" migliore è molto soggettivo, c'è ancora una differenza abbastanza grande nella funzionalità. Prendi il seguente esempio:

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

Il ciclo while si trova all'interno del blocco try catch, la variabile 'j' viene incrementata fino a raggiungere 40, stampata quando j mod 4 è zero e viene generata un'eccezione quando j colpisce 20.

Prima di ogni dettaglio, ecco l'altro esempio:

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

Stessa logica di cui sopra, l'unica differenza è che il blocco try / catch è ora all'interno del ciclo while.

Ecco l'output (mentre in try / catch):

4
8
12 
16
in catch block

E l'altro output (prova / intercetta mentre):

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

Lì hai una differenza abbastanza significativa:

mentre in try / catch esce dal ciclo

try / catch in mentre mantiene attivo il loop


Quindi mettiti try{} catch() {}intorno al loop se il loop viene trattato come una singola "unità" e la ricerca di un problema in quell'unità fa sì che l'intera "unità" (o loop in questo caso) vada a sud. Inserisci try{} catch() {}all'interno del ciclo se stai trattando una singola iterazione del ciclo o un singolo elemento in un array come un'intera "unità". Se si verifica un'eccezione in tale "unità", saltiamo semplicemente quell'elemento e quindi continuiamo alla successiva iterazione del ciclo.
Galaxy

14

Sono d'accordo con tutti i post sulle prestazioni e la leggibilità. Tuttavia, ci sono casi in cui conta davvero. Un paio di altre persone lo hanno menzionato, ma potrebbe essere più facile vederlo con degli esempi.

Considera questo esempio leggermente modificato:

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

Se vuoi che il metodo parseAll () restituisca null se ci sono errori (come nell'esempio originale), dovresti mettere il try / catch all'esterno in questo modo:

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 realtà, dovresti probabilmente restituire un errore qui invece di null, e generalmente non mi piace avere più resi, ma hai l'idea.

D'altra parte, se vuoi che ignori i problemi e analizzi qualunque stringa possibile, avresti messo il try / catch all'interno del loop in questo modo:

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

2
Ecco perché ho detto "Se vuoi semplicemente catturare un valore negativo ma continuare l'elaborazione, inseriscilo".
Ray Hayes,

5

Come già accennato, le prestazioni sono le stesse. Tuttavia, l'esperienza dell'utente non è necessariamente identica. Nel primo caso, fallirai rapidamente (cioè dopo il primo errore), tuttavia se inserisci il blocco try / catch all'interno del ciclo, puoi catturare tutti gli errori che verrebbero creati per una determinata chiamata al metodo. Quando analizzi una matrice di valori da stringhe in cui ti aspetti alcuni errori di formattazione, ci sono sicuramente casi in cui ti piacerebbe essere in grado di presentare tutti gli errori all'utente in modo che non debbano provare a risolverli uno per uno .


Questo non è vero per questo caso particolare (sto tornando nel blocco catch), ma è una buona cosa da tenere a mente in generale.
Michael Myers

4

Se fallisce tutto o niente, allora il primo formato ha senso. Se si desidera essere in grado di elaborare / restituire tutti gli elementi non in errore, è necessario utilizzare il secondo modulo. Questi sarebbero i miei criteri di base per scegliere tra i metodi. Personalmente, se fosse tutto o niente, non userei il secondo modulo.


4

Fintanto che sei consapevole di ciò che devi realizzare nel loop, puoi mettere il tentativo try al di fuori del loop. Ma è importante capire che il ciclo terminerà non appena si verifica l'eccezione e che potrebbe non essere sempre quello che desideri. Questo è in realtà un errore molto comune nel software basato su Java. Le persone devono elaborare una serie di elementi, come svuotare una coda, e fare affidamento su un'istruzione try / catch esterna che gestisce tutte le possibili eccezioni. Potrebbero anche gestire solo un'eccezione specifica all'interno del ciclo e non aspettarsi che si verifichino altre eccezioni. Quindi, se si verifica un'eccezione che non viene gestita all'interno del ciclo, il ciclo verrà "prevenuto", termina probabilmente prematuramente e l'istruzione catch esterna gestisce l'eccezione.

Se il ciclo avesse il suo ruolo nella vita di svuotare una coda, molto probabilmente quel ciclo potrebbe finire prima che quella coda fosse davvero svuotata. Errore molto comune.


2

Nei tuoi esempi non c'è differenza funzionale. Trovo il tuo primo esempio più leggibile.


2

Dovresti preferire la versione esterna alla versione interna. Questa è solo una versione specifica della regola, sposta qualsiasi cosa al di fuori del ciclo che puoi spostare al di fuori del ciclo. A seconda del compilatore IL e del compilatore JIT, le due versioni potrebbero o meno finire con caratteristiche prestazionali diverse.

In un'altra nota, dovresti probabilmente guardare float. TryParse o Convert.ToFloat.


2

Se inserisci il try / catch all'interno del loop, continuerai a eseguire il loop dopo un'eccezione. Se lo metti fuori dal ciclo, ti fermerai non appena viene generata un'eccezione.


Bene, sto restituendo null su eccezione, quindi non continuerà l'elaborazione nel mio caso.
Michael Myers

2

La mia prospettiva sarebbe che i blocchi try / catch siano necessari per assicurare una corretta gestione delle eccezioni, ma la creazione di tali blocchi ha implicazioni sulle prestazioni. Poiché, i loop contengono calcoli ripetitivi intensivi, non è consigliabile inserire blocchi try / catch all'interno dei loop. Inoltre, sembra che si verifichi questa condizione, spesso viene rilevata "Eccezione" o "RuntimeException". RuntimeException catturata nel codice dovrebbe essere evitata. Ancora una volta, se si lavora in una grande azienda, è essenziale registrare correttamente tale eccezione o arrestare l'eccezione di runtime. L'intero punto di questa descrizione èPLEASE AVOID USING TRY-CATCH BLOCKS IN LOOPS


1

la configurazione di un frame stack speciale per il tentativo / cattura aggiunge un ulteriore sovraccarico, ma la JVM potrebbe essere in grado di rilevare il fatto che stai tornando e ottimizzarlo via.

a seconda del numero di iterazioni, la differenza di prestazioni sarà probabilmente trascurabile.

Tuttavia, concordo con gli altri sul fatto che averlo al di fuori del loop rende il corpo del loop più pulito.

Se c'è la possibilità che tu voglia continuare con l'elaborazione piuttosto che uscire se c'è un numero non valido, allora vorresti che il codice fosse all'interno del ciclo.


1

Se è all'interno, otterrai il sovraccarico della struttura try / catch N volte, al contrario di una sola volta all'esterno.


Ogni volta che viene chiamata una struttura Try / Catch, si aggiunge un sovraccarico all'esecuzione del metodo. Solo un po 'di memoria e processore tick necessari per gestire la struttura. Se stai eseguendo un loop 100 volte e, per amor di ipotesi, supponiamo che il costo sia 1 tick per ogni chiamata try / catch, quindi avere il Try / Catch all'interno del loop ti costa 100 tick, invece di solo 1 tick se è fuori dal giro.


Puoi approfondire un po 'le spese generali?
Michael Myers

1
Va bene, ma Jeffrey L Whitledge dice che non c'è nessun spese extra. Potete fornire una fonte che dice che c'è?
Michael Myers

Il costo è piccolo, quasi trascurabile, ma è lì - tutto ha un costo. Ecco un articolo sul blog di Programmer's Heaven ( tiny.cc/ZBX1b ) relativo a .NET ed ecco un post di newsgroup da Reshat Sabiq riguardante JAVA ( tiny.cc/BV2hS )
Stephen Wrighton,

Capisco ... Quindi ora la domanda è: è il costo sostenuto per entrare nel try / catch (nel qual caso dovrebbe essere al di fuori del ciclo), oppure è costante per la durata del try / catch (nel qual caso dovrebbe essere all'interno del ciclo)? Dici che è il numero 1. E non sono ancora del tutto convinto che ci sia un costo.
Michael Myers

1
Secondo questo Google Book ( tinyurl.com/3pp7qj ), "le VM più recenti non mostrano alcuna penalità".
Michael Myers

1

L'intero punto delle eccezioni è incoraggiare il primo stile: consentire che la gestione degli errori sia consolidata e gestita una volta, non immediatamente in ogni possibile sito di errore.


Sbagliato! Il punto delle eccezioni è determinare quale comportamento eccezionale adottare quando si verifica un "errore". Quindi la scelta del blocco try / catch interno o esterno dipende davvero da come si desidera gestire il trattamento ad anello.
gizmo,

Questo può essere fatto ugualmente bene con i trattamenti di errore pre-eccezione, come il passaggio di flag "si è verificato un errore".
wnoise,

1

mettilo dentro. È possibile continuare l'elaborazione (se lo si desidera) oppure generare un'utile eccezione che indica al client il valore di myString e l'indice dell'array contenente il valore errato. Penso che NumberFormatException ti dirà già il valore negativo, ma il principio è quello di inserire tutti i dati utili nelle eccezioni che lanci. Pensa a cosa sarebbe interessante per te nel debugger a questo punto del programma.

Tener conto di:

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

Nel momento del bisogno apprezzerai davvero un'eccezione come questa con quante più informazioni possibili.


Sì, anche questo è un punto valido. Nel mio caso sto leggendo da un file, quindi tutto ciò di cui ho veramente bisogno è la stringa che non è riuscita ad analizzare. Così ho fatto System.out.println (ex) invece di lanciare un'altra eccezione (voglio analizzare tanto il file quanto è legale).
Michael Myers

1

Mi piacerebbe aggiungere il mio 0.02csu due considerazioni concorrenti quando guardo al problema generale di dove posizionare la gestione delle eccezioni:

  1. La "più ampia" responsabilità del try-catchblocco (cioè all'esterno del loop nel tuo caso) significa che quando si modifica il codice in un momento successivo, è possibile aggiungere erroneamente una riga che viene gestita dal catchblocco esistente ; forse involontariamente. Nel tuo caso, questo è meno probabile perché stai catturando esplicitamente aNumberFormatException

  2. Più "stretta" è la responsabilità del try-catchblocco, più diventa difficile il refactoring. Soprattutto quando (come nel tuo caso) stai eseguendo un'istruzione "non locale" dall'interno del catchblocco (l' return nullistruzione).


1

Dipende dalla gestione degli errori. Se vuoi solo saltare gli elementi di errore, prova dentro:

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

In ogni caso preferirei provare fuori. Il codice è più leggibile, è più pulito. Forse sarebbe meglio lanciare un IllegalArgumentException nel caso dell'errore se restituisce null.


1

Metterò i miei $ 0,02 in. A volte finisci per dover aggiungere un "finalmente" più avanti nel tuo codice (perché chi mai scrive il loro codice perfettamente la prima volta?). In questi casi, improvvisamente ha più senso avere il tentativo / cattura al di fuori del ciclo. Per esempio:

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

Perché se ricevi un errore o no, vuoi rilasciare la tua connessione al database (o scegliere il tuo tipo preferito di altra risorsa ...) una volta.


1

Un altro aspetto non menzionato in precedenza è il fatto che ogni tentativo di cattura ha un certo impatto sullo stack, che può avere implicazioni per i metodi ricorsivi.

Se il metodo "esterno ()" chiama il metodo "interno ()" (che può chiamarsi ricorsivamente), provare a individuare il metodo try-catch nel metodo "esterno ()" se possibile. Un semplice esempio di "crash dello stack" che utilizziamo in una classe di prestazioni fallisce a circa 6.400 frame quando il metodo try-catch è nel metodo interno e a circa 11.600 quando è nel metodo esterno.

Nel mondo reale, questo può essere un problema se stai usando il modello composito e hai strutture nidificate grandi e complesse.


0

Se vuoi catturare Eccezione per ogni iterazione o controllare a quale iterazione viene generata Eccezione e catturare tutte le Eccezioni in un itertaion, posiziona provare ... catch all'interno del loop. Questo non interromperà il ciclo se si verifica l'eccezione e puoi catturare ogni eccezione in ogni iterazione durante il ciclo.

Se vuoi interrompere il loop ed esaminare l'eccezione ogni volta che viene lanciato, usa try ... catch out of the loop. Ciò interromperà il ciclo ed eseguirà le istruzioni dopo la cattura (se presenti).

Tutto dipende dalle tue necessità. Preferisco usare try ... catch all'interno del ciclo durante la distribuzione poiché, se si verifica Eccezione, i risultati non sono ambigui e il ciclo non si interromperà e verrà eseguito completamente.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.