Devrait essayer… attraper à l'intérieur ou à l'extérieur d'une boucle?

185

J'ai une boucle qui ressemble à ceci:

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

C'est le contenu principal d'une méthode dont le seul but est de renvoyer le tableau de flottants. Je veux que cette méthode retourne nulls'il y a une erreur, donc je mets la boucle dans un try...catchbloc, comme ceci:

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

Mais j'ai aussi pensé à mettre le try...catchbloc à l'intérieur de la boucle, comme ceci:

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

Y a-t-il une raison, une performance ou autre, de préférer l'un à l'autre?


Edit: Le consensus semble être qu'il est plus propre de mettre la boucle à l'intérieur du try / catch, éventuellement dans sa propre méthode. Cependant, il y a encore un débat sur lequel est le plus rapide. Quelqu'un peut-il tester cela et revenir avec une réponse unifiée?

Michael Myers
la source
2
Je ne connais pas les performances, mais le code semble plus propre avec la capture d'essai en dehors de la boucle for.
Jack B Nimble le

Réponses:

132

PERFORMANCE:

Il n'y a absolument aucune différence de performance dans l'emplacement des structures try / catch. En interne, ils sont implémentés en tant que table de plages de codes dans une structure créée lorsque la méthode est appelée. Pendant que la méthode s'exécute, les structures try / catch sont complètement hors de l'image à moins qu'un renvoi ne se produise, puis l'emplacement de l'erreur est comparé à la table.

Voici une référence: http://www.javaworld.com/javaworld/jw-01-1997/jw-01-hood.html

Le tableau est décrit à mi-chemin environ.

Jeffrey L Whitledge
la source
D'accord, alors vous dites qu'il n'y a pas de différence sauf l'esthétique. Pouvez-vous créer un lien vers une source?
Michael Myers
2
Merci. Je suppose que les choses ont peut-être changé depuis 1997, mais elles ne le rendraient guère moins efficace.
Michael Myers
1
C'est absolument un mot difficile. Dans certains cas, cela peut empêcher les optimisations du compilateur. Ce n'est pas un coût direct, mais cela peut être une pénalité de performance indirecte.
Tom Hawtin - tackline
2
Certes, j'imagine que j'aurais dû régner un peu sur mon enthousiasme, mais la désinformation m'énervait! Dans le cas des optimisations, puisque nous ne pouvons pas savoir de quelle manière les performances seraient affectées, je suppose que nous sommes revenus pour l'essayer et le tester (comme toujours).
Jeffrey L Whitledge le
1
il n'y a pas de différence de performances, uniquement si le code s'exécute sans exceptions.
Onur Bıyık
72

Performance : comme l'a dit Jeffrey dans sa réponse, en Java, cela ne fait pas beaucoup de différence.

En règle générale , pour la lisibilité du code, votre choix de l'endroit où intercepter l'exception dépend de si vous voulez que la boucle continue le traitement ou non.

Dans votre exemple, vous êtes revenu après avoir détecté une exception. Dans ce cas, je mettrais le try / catch autour de la boucle. Si vous voulez simplement attraper une mauvaise valeur mais continuer le traitement, mettez-la à l'intérieur.

La troisième façon : vous pouvez toujours écrire votre propre méthode statique ParseFloat et gérer la gestion des exceptions dans cette méthode plutôt que dans votre boucle. Rendre la gestion des exceptions isolée de la boucle elle-même!

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;
    }
}
Ray Hayes
la source
1
Regarder de plus près. Il sort sur la première exception des deux. J'ai pensé la même chose aussi au début.
kervin le
2
J'étais conscient, c'est pourquoi j'ai dit dans son cas, puisqu'il revient quand il rencontre un problème alors j'utiliserais une capture extérieure. Pour plus de clarté, c'est la règle que j'utiliserais ...
Ray Hayes
Bon code, mais malheureusement Java n'a pas le luxe de sortir des paramètres.
Michael Myers
ByRef? Cela fait si longtemps que je n'ai pas touché à Java!
Ray Hayes du
2
Quelqu'un d'autre a suggéré TryParse qui en C # /. Net vous permet de faire une analyse sécurisée sans traiter les exceptions, qui est maintenant répliqué et la méthode est réutilisable. En ce qui concerne la question d'origine, je préférerais la boucle à l'intérieur de la prise, elle a juste l'air plus propre même s'il n'y a pas de problème de performance ..
Ray Hayes
46

D'accord, après que Jeffrey L Whitledge ait dit qu'il n'y avait pas de différence de performance (à partir de 1997), je suis allé le tester. J'ai couru ce petit 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;
        }
    }
}

J'ai vérifié le bytecode résultant en utilisant javap pour m'assurer que rien n'était incorporé.

Les résultats ont montré que, en supposant des optimisations JIT insignifiantes, Jeffrey a raison ; il n'y a absolument aucune différence de performances sur Java 6, machine virtuelle client Sun (je n'avais pas accès à d'autres versions). La différence de temps totale est de l'ordre de quelques millisecondes sur l'ensemble du test.

Par conséquent, la seule considération est ce qui semble le plus propre. Je trouve que la deuxième manière est moche, donc je m'en tiendrai à la première ou à celle de Ray Hayes .

Michael Myers
la source
Si le gestionnaire d'exceptions prend le flux en dehors de la boucle, alors je pense qu'il est plus clair de le placer en dehors de la boucle - Plutôt que de placer une clause catch apparemment dans la boucle, qui retourne néanmoins. J'utilise une ligne d'espaces autour du corps "attrapé" de ces méthodes "fourre-tout", pour séparer plus clairement le corps du bloc try englobant tout .
Thomas W
15

Bien que les performances puissent être les mêmes et que ce qui «semble» le mieux soit très subjectif, il y a encore une assez grande différence de fonctionnalité. Prenons l'exemple suivant:

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

La boucle while est à l'intérieur du bloc try catch, la variable 'j' est incrémentée jusqu'à ce qu'elle atteigne 40, imprimée lorsque j mod 4 est à zéro et une exception est levée lorsque j atteint 20.

Avant tout détail, voici l'autre exemple:

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

Même logique que ci-dessus, la seule différence est que le bloc try / catch est maintenant à l'intérieur de la boucle while.

Voici la sortie (en try / catch):

4
8
12 
16
in catch block

Et l'autre sortie (try / catch in while):

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

Là, vous avez une différence assez significative:

tandis que dans try / catch sort de la boucle

try / catch in while garde la boucle active

seBaka28
la source
1
Donc, mettez try{} catch() {}autour de la boucle si la boucle est traitée comme une seule "unité" et trouver un problème dans cette unité fait que toute "l'unité" (ou la boucle dans ce cas) se dirige vers le sud. Mettez try{} catch() {}à l'intérieur de la boucle si vous traitez une seule itération de la boucle, ou un seul élément d'un tableau comme une «unité» entière. Si une exception dans cette "unité" se produit, nous sautons simplement cet élément, puis nous continuons à l'itération suivante de la boucle.
Galaxy
14

Je suis d'accord avec tous les articles sur les performances et la lisibilité. Cependant, il y a des cas où cela importe vraiment. Quelques autres personnes l'ont mentionné, mais cela pourrait être plus facile à voir avec des exemples.

Considérez cet exemple légèrement modifié:

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

Si vous voulez que la méthode parseAll () renvoie null s'il y a des erreurs (comme l'exemple d'origine), vous placez le try / catch à l'extérieur comme ceci:

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

En réalité, vous devriez probablement renvoyer une erreur ici au lieu de null, et généralement je n'aime pas avoir plusieurs retours, mais vous voyez l'idée.

D'un autre côté, si vous voulez qu'il ignore simplement les problèmes et analyse toutes les chaînes qu'il peut, vous placez le try / catch à l'intérieur de la boucle comme ceci:

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;
}
Matt N
la source
2
C'est pourquoi j'ai dit "Si vous voulez simplement attraper une mauvaise valeur mais continuer le traitement, mettez-la à l'intérieur."
Ray Hayes
5

Comme déjà mentionné, les performances sont les mêmes. Cependant, l'expérience utilisateur n'est pas nécessairement identique. Dans le premier cas, vous échouerez rapidement (c'est-à-dire après la première erreur), cependant si vous placez le bloc try / catch dans la boucle, vous pouvez capturer toutes les erreurs qui seraient créées pour un appel donné à la méthode. Lors de l'analyse d'un tableau de valeurs à partir de chaînes où vous vous attendez à des erreurs de formatage, il y a certainement des cas où vous souhaitez pouvoir présenter toutes les erreurs à l'utilisateur afin qu'il n'ait pas besoin d'essayer de les corriger une par une .

user19810
la source
Ce n'est pas vrai pour ce cas particulier (je reviens dans le bloc catch), mais c'est une bonne chose à garder à l'esprit en général.
Michael Myers
4

Si c'est un échec du tout ou rien, alors le premier format a du sens. Si vous souhaitez pouvoir traiter / renvoyer tous les éléments non défaillants, vous devez utiliser le deuxième formulaire. Ce seraient mes critères de base pour choisir entre les méthodes. Personnellement, si c'est tout ou rien, je n'utiliserais pas la deuxième forme.

Joe Skora
la source
4

Tant que vous êtes conscient de ce que vous devez accomplir dans la boucle, vous pouvez mettre le crochet d'essai en dehors de la boucle. Mais il est important de comprendre que la boucle se terminera alors dès que l'exception se produira et que ce n'est peut-être pas toujours ce que vous souhaitez. Il s'agit en fait d'une erreur très courante dans les logiciels Java. Les utilisateurs doivent traiter un certain nombre d'éléments, tels que vider une file d'attente, et s'appuyer à tort sur une instruction try / catch externe gérant toutes les exceptions possibles. Ils peuvent également ne gérer qu'une exception spécifique à l'intérieur de la boucle et ne pas s'attendre à ce qu'une autre exception se produise. Ensuite, si une exception se produit qui n'est pas gérée à l'intérieur de la boucle, alors la boucle sera "pré-émise", elle se terminera peut-être prématurément et l'instruction catch externe gère l'exception.

Si la boucle avait pour rôle dans la vie de vider une file d'attente, alors cette boucle pourrait très probablement se terminer avant que cette file d'attente ne soit réellement vidée. Faute très courante.

Gunnar Forsgren - Mobimation
la source
2

Dans vos exemples, il n'y a pas de différence fonctionnelle. Je trouve votre premier exemple plus lisible.

Jamie
la source
2

Vous devriez préférer la version externe à la version interne. Ceci est juste une version spécifique de la règle, déplacez tout ce qui est en dehors de la boucle que vous pouvez déplacer en dehors de la boucle. En fonction du compilateur IL et du compilateur JIT, vos deux versions peuvent ou non se retrouver avec des caractéristiques de performances différentes.

Sur une autre note, vous devriez probablement regarder float.TryParse ou Convert.ToFloat.

Orion Adrian
la source
2

Si vous mettez le try / catch dans la boucle, vous continuerez à boucler après une exception. Si vous le mettez en dehors de la boucle, vous vous arrêterez dès qu'une exception est levée.

Joël Coehoorn
la source
Eh bien, je retourne null sur exception, donc il ne continuerait pas le traitement dans mon cas.
Michael Myers
2

Mon point de vue serait que les blocs try / catch sont nécessaires pour assurer une gestion correcte des exceptions, mais la création de tels blocs a des implications sur les performances. Puisque les boucles contiennent des calculs répétitifs intensifs, il n'est pas recommandé de mettre des blocs try / catch à l'intérieur des boucles. De plus, il semble que là où cette condition se produit, c'est souvent «Exception» ou «RuntimeException» qui est interceptée. RuntimeException interceptée dans le code doit être évitée. Encore une fois, si vous travaillez dans une grande entreprise, il est essentiel de consigner correctement cette exception ou d'arrêter que l'exception d'exécution se produise. Le point entier de cette description estPLEASE AVOID USING TRY-CATCH BLOCKS IN LOOPS

surendrapanday
la source
1

la configuration d'un cadre de pile spécial pour le try / catch ajoute une surcharge supplémentaire, mais la JVM peut être en mesure de détecter le fait que vous revenez et d'optimiser cela.

selon le nombre d'itérations, la différence de performance sera probablement négligeable.

Cependant, je suis d'accord avec les autres pour dire que l'avoir en dehors de la boucle rend le corps de la boucle plus propre.

S'il y a une chance que vous souhaitiez un jour continuer le traitement plutôt que de quitter s'il y a un numéro invalide, alors vous voudrez que le code soit à l'intérieur de la boucle.

Mat
la source
1

Si c'est à l'intérieur, vous gagnerez N fois la surcharge de la structure try / catch, par opposition à une seule fois à l'extérieur.


Chaque fois qu'une structure Try / Catch est appelée, cela ajoute une surcharge à l'exécution de la méthode. Juste le petit peu de mémoire et de processeur nécessaires pour gérer la structure. Si vous exécutez une boucle 100 fois, et pour des raisons hypothétiques, disons que le coût est de 1 tick par appel try / catch, alors avoir le Try / Catch à l'intérieur de la boucle vous coûte 100 ticks, par opposition à seulement 1 tick si c'est en dehors de la boucle.

Stephen Wrighton
la source
Pouvez-vous élaborer un peu sur les frais généraux?
Michael Myers
1
D'accord, mais Jeffrey L Whitledge dit qu'il n'y a pas de frais généraux supplémentaires. Pouvez-vous fournir une source qui dit qu'il y en a?
Michael Myers
Le coût est faible, presque négligeable, mais il est là - tout a un coût. Voici un article de blog de Programmer's Heaven ( tiny.cc/ZBX1b ) concernant .NET et voici un article de groupe de discussion de Reshat Sabiq concernant JAVA ( tiny.cc/BV2hS )
Stephen Wright le
Je vois ... Alors maintenant, la question est, est-ce que le coût encouru pour entrer dans le try / catch (auquel cas il devrait être en dehors de la boucle), ou est-il constant pour la durée du try / catch (auquel cas il devrait être à l'intérieur de la boucle)? Vous dites que c'est n ° 1. Et je ne suis toujours pas entièrement convaincu qu'il y a un coût.
Michael Myers
1
Selon ce Google Book ( tinyurl.com/3pp7qj ), "les dernières VM ne montrent aucune pénalité".
Michael Myers
1

Le but des exceptions est d'encourager le premier style: laisser la gestion des erreurs être consolidée et traitée une fois, pas immédiatement sur chaque site d'erreur possible.

wnoise
la source
Faux! Le but des exceptions est de déterminer le comportement exceptionnel à adopter lorsqu'une «erreur» se produit. Ainsi, le choix du bloc try / catch interne ou externe dépend vraiment de la manière dont vous souhaitez gérer le traitement de la boucle.
gizmo
Cela peut être fait aussi bien avec des traitements d'erreur pré-exception, tels que le passage des indicateurs «erreur s'est produite».
wnoise du
1

mets le dedans. Vous pouvez continuer le traitement (si vous le souhaitez) ou vous pouvez lancer une exception utile qui indique au client la valeur de myString et l'index du tableau contenant la mauvaise valeur. Je pense que NumberFormatException vous indiquera déjà la mauvaise valeur mais le principe est de placer toutes les données utiles dans les exceptions que vous lancez. Pensez à ce qui vous intéresserait dans le débogueur à ce stade du programme.

Considérer:

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

En cas de besoin, vous apprécierez vraiment une exception comme celle-ci avec autant d'informations que possible.

Kyle Dyer
la source
Oui, c'est également un point valable. Dans mon cas, je lis à partir d'un fichier, donc tout ce dont j'ai vraiment besoin est la chaîne qui n'a pas pu être analysée. J'ai donc fait System.out.println (ex) au lieu de lancer une autre exception (je veux analyser autant de fichiers que possible).
Michael Myers
1

J'aime ajouter le mien à 0.02cpropos de deux considérations concurrentes lorsque je regarde le problème général de l'emplacement de la gestion des exceptions:

  1. La responsabilité «plus large» du try-catchbloc (c'est-à-dire en dehors de la boucle dans votre cas) signifie que lorsque vous changez le code ultérieurement, vous pouvez ajouter par erreur une ligne qui est gérée par votre catchbloc existant ; peut-être involontairement. Dans votre cas, c'est moins probable car vous attrapez explicitement unNumberFormatException

  2. Plus la responsabilité du try-catchbloc est "étroite" , plus la refactorisation devient difficile. En particulier lorsque (comme dans votre cas) vous exécutez une instruction "non locale" depuis le catchbloc (l' return nullinstruction).

oxbow_lakes
la source
1

Cela dépend de la gestion des échecs. Si vous voulez simplement ignorer les éléments d'erreur, essayez à l'intérieur:

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

Dans tous les autres cas, je préférerais essayer à l'extérieur. Le code est plus lisible, il est plus propre. Il serait peut-être préférable de lancer une IllegalArgumentException dans le cas d'erreur à la place si vous renvoyez null.

Arne Burmeister
la source
1

Je vais mettre mon 0,02 $. Parfois, vous finissez par avoir besoin d'ajouter un "enfin" plus tard dans votre code (car qui a déjà écrit parfaitement son code la première fois?). Dans ces cas, il est tout à coup plus logique d'avoir le try / catch en dehors de la boucle. Par exemple:

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

Parce que si vous obtenez une erreur, ou non, vous ne voulez libérer votre connexion à la base de données (ou choisir votre type préféré d'autre ressource ...) qu'une seule fois.

Ogre Psaume33
la source
1

Un autre aspect non mentionné dans ce qui précède est le fait que chaque try-catch a un impact sur la pile, ce qui peut avoir des implications pour les méthodes récursives.

Si la méthode "external ()" appelle la méthode "inner ()" (qui peut s'appeler elle-même de manière récursive), essayez si possible de localiser le try-catch dans la méthode "external ()". Un simple exemple de «crash de pile» que nous utilisons dans une classe de performances échoue à environ 6 400 images lorsque le try-catch est dans la méthode interne, et à environ 11 600 lorsqu'il est dans la méthode externe.

Dans le monde réel, cela peut être un problème si vous utilisez le modèle composite et que vous avez de grandes structures imbriquées complexes.

Jeff Hill
la source
0

Si vous voulez attraper une exception pour chaque itération, ou vérifier à quelle itération Exception est levée et attraper toutes les exceptions dans une itération, placez try ... catch dans la boucle. Cela ne rompra pas la boucle si une exception se produit et vous pouvez intercepter chaque exception à chaque itération tout au long de la boucle.

Si vous souhaitez interrompre la boucle et examiner l'exception chaque fois qu'elle est lancée, utilisez try ... catch hors de la boucle. Cela rompra la boucle et exécutera des instructions après catch (le cas échéant).

Tout dépend de vos besoins. Je préfère utiliser try ... catch dans la boucle lors du déploiement car, si une exception se produit, les résultats ne sont pas ambigus et la boucle ne se cassera pas et ne s'exécutera pas complètement.

Juned Khan Momin
la source