Recherche…


Introduction

Plusieurs abus de langage de programmation Java peuvent conduire à un programme pour générer des résultats incorrects malgré la compilation correcte. L'objectif principal de ce sujet est de répertorier les pièges courants liés à la gestion des exceptions et de proposer la manière correcte d'éviter de tels pièges.

Piège - Ignorer ou écraser les exceptions

Cet exemple concerne le fait d’ignorer ou d’écraser délibérément des exceptions. Ou, pour être plus précis, il s'agit de savoir comment capturer et gérer une exception de manière à l'ignorer. Cependant, avant de décrire comment faire cela, nous devons d'abord souligner que les exceptions pour écraser ne sont généralement pas la bonne façon de les gérer.

Des exceptions sont généralement émises (par quelque chose) pour notifier aux autres parties du programme qu'un événement significatif ("exceptionnel") s'est produit. Généralement (mais pas toujours) une exception signifie que quelque chose a mal tourné. Si vous codez votre programme pour écraser l'exception, il y a de fortes chances que le problème réapparaisse sous une autre forme. Pour aggraver les choses, lorsque vous écrasez l'exception, vous jetez les informations dans l'objet exception et sa trace de pile associée. Cela risque de rendre plus difficile l’identification de la source du problème.

En pratique, les squashings d'exceptions se produisent souvent lorsque vous utilisez la fonctionnalité de correction automatique de l'EDI pour "corriger" une erreur de compilation provoquée par une exception non gérée. Par exemple, vous pourriez voir du code comme ceci:

try {
    inputStream = new FileInputStream("someFile");
} catch (IOException e) {
    /* add exception handling code here */
}

Clairement, le programmeur a accepté la suggestion de l'IDE de supprimer l'erreur de compilation, mais la suggestion était inappropriée. (Si le fichier ouvert a échoué, le programme devrait probablement faire quelque chose à ce sujet. Avec la "correction" ci-dessus, le programme risque d'échouer ultérieurement, par exemple avec une inputStream NullPointerException car inputStream est maintenant null .)

Cela dit, voici un exemple d’écrasement délibéré d’une exception. (Aux fins de l'argumentation, supposons que nous avons déterminé qu'une interruption lors de l'affichage du selfie est inoffensive.) Le commentaire indique au lecteur que nous avons écrasé l'exception délibérément et pourquoi nous l'avons fait.

try {
    selfie.show();
} catch (InterruptedException e) {
    // It doesn't matter if showing the selfie is interrupted.
}

Une autre manière conventionnelle de souligner que nous avons délibérément écrasé une exception sans dire pourquoi est d’indiquer ceci avec le nom de la variable d’exception, comme ceci:

try { 
    selfie.show(); 
} catch (InterruptedException ignored) {  }

Certains IDE (comme IntelliJ IDEA) n’afficheront pas d’avertissement concernant le bloc catch vide si le nom de la variable est défini sur ignored .

Piège - Attraper une exception pouvant être lancée, une exception ou une erreur d'exécution

Un modèle de pensée commune pour les programmeurs Java inexpérimentés est que les exceptions sont « un problème » ou « un fardeau » et la meilleure façon de traiter ce problème est de les attraper tous 1 le plus tôt possible. Cela conduit à un code comme celui-ci:

....
try {
    InputStream is = new FileInputStream(fileName);
    // process the input
} catch (Exception ex) {
    System.out.println("Could not open file " + fileName);
}

Le code ci-dessus présente un défaut important. Le catch va en fait attraper plus d'exceptions que ce que le programmeur attend. Supposons que la valeur du nom de fileName est null , en raison d'un bogue ailleurs dans l'application. Cela entraînera le constructeur FileInputStream à lancer une FileInputStream NullPointerException . Le gestionnaire intercepte ceci et signale à l'utilisateur:

    Could not open file null

ce qui est inutile et déroutant. Pire encore, supposons que ce soit le code "traiter l'entrée" qui a déclenché l'exception inattendue (cochée ou décochée!). Maintenant, l'utilisateur recevra le message trompeur pour un problème qui ne s'est pas produit lors de l'ouverture du fichier, et peut ne pas avoir de lien avec les E / S.

La racine du problème est que le programmeur a codé un gestionnaire pour Exception . C'est presque toujours une erreur:

  • Catching Exception intercepte toutes les exceptions vérifiées et la plupart des exceptions non vérifiées.
  • Attraper RuntimeException intercepte la plupart des exceptions non RuntimeException .
  • Catching Error détecte les exceptions non vérifiées qui signalent les erreurs internes de la JVM. Ces erreurs ne sont généralement pas récupérables et ne doivent pas être interceptées.
  • Catching Throwable interceptera toutes les exceptions possibles.

Le problème lié à un ensemble trop large d'exceptions est que le gestionnaire ne peut généralement pas tous les gérer correctement. Dans le cas de l' Exception , etc., il est difficile pour le programmeur de prédire ce qui pourrait être intercepté. c'est-à-dire à quoi s'attendre.

En général, la bonne solution est de traiter les exceptions levées. Par exemple, vous pouvez les attraper et les manipuler sur place:

try {
    InputStream is = new FileInputStream(fileName);
    // process the input
} catch (FileNotFoundException ex) {
    System.out.println("Could not open file " + fileName);
}

ou vous pouvez les déclarer comme thrown par la méthode


Il existe très peu de situations où la capture d’ Exception est appropriée. Le seul qui se présente couramment est quelque chose comme ceci:

public static void main(String[] args) {
    try {
        // do stuff
    } catch (Exception ex) {
        System.err.println("Unfortunately an error has occurred. " +
                           "Please report this to X Y Z");
        // Write stacktrace to a log file.
        System.exit(1);
    }
}

Ici, nous voulons vraiment gérer toutes les exceptions, donc capturer Exception (ou même Throwable ) est correct.


1 - Aussi connu sous le nom de gestion des exceptions Pokemon .

Piège - Lancer Throwable, Exception, Error ou RuntimeException

Bien que la capture des Exception Throwable , Exception , Error et RuntimeException soit mauvaise, leur RuntimeException est encore pire.

Le problème de base est que lorsque votre application doit gérer des exceptions, la présence des exceptions de niveau supérieur rend difficile la distinction entre différentes conditions d'erreur. Par exemple

try {
    InputStream is = new FileInputStream(someFile);  // could throw IOException
    ...
    if (somethingBad) {
        throw new Exception();  // WRONG
    }
} catch (IOException ex) {
    System.err.println("cannot open ...");
} catch (Exception ex) {
    System.err.println("something bad happened");  // WRONG
}

Le problème est que parce que nous avons lancé une instance Exception , nous sommes obligés de l'attraper. Cependant, comme décrit dans un autre exemple, la détection des Exception est mauvaise. Dans ce cas, il devient difficile de faire la distinction entre le cas "attendu" d'une Exception déclenchée si somethingBad est true et le cas inattendu où nous interceptons une exception non vérifiée telle que NullPointerException .

Si l'exception de niveau supérieur est autorisée à se propager, nous rencontrons d'autres problèmes:

  • Nous devons maintenant nous souvenir de toutes les différentes raisons pour lesquelles nous avons lancé le plus haut niveau et les discriminer / gérer.
  • Dans le cas d' Exception et Throwable nous devons aussi ajouter ces exceptions à la throws clause de méthodes si nous voulons l'exception de se propager. Ceci est problématique, comme décrit ci-dessous.

En bref, ne jetez pas ces exceptions. Jetez une exception plus spécifique qui décrit plus précisément "l'événement exceptionnel" qui s'est produit. Si vous devez, définissez et utilisez une classe d'exception personnalisée.

Déclarer Throwable ou Exception dans les "lancers" d'une méthode est problématique.

Il est tentant de remplacer une longue liste d'exceptions lancées dans une méthode de throws clause avec Exception ou même `Throwable. C'est une mauvaise idée:

  1. Il force l'appelant à gérer (ou propager) les Exception .
  2. Nous ne pouvons plus compter sur le compilateur pour nous informer des exceptions vérifiées spécifiques à gérer.
  3. Manipuler correctement l' Exception est difficile. Il est difficile de savoir quelles exceptions réelles peuvent être interceptées et, si vous ne savez pas ce qui pourrait être capturé, il est difficile de savoir quelle stratégie de rétablissement est appropriée.
  4. Manipulation Throwable est encore plus difficile, car vous devez maintenant faire face à des défaillances potentielles qui ne devraient jamais être récupérées.

Ce conseil signifie que certains autres modèles doivent être évités. Par exemple:

try {
    doSomething();
} catch (Exception ex) {
    report(ex);
    throw ex;
}

Les tentatives ci-dessus tentent de consigner toutes les exceptions au fur et à mesure qu'elles passent, sans les traiter définitivement. Malheureusement, avant Java 7, le throw ex; La déclaration a amené le compilateur à penser que toute Exception pouvait être lancée. Cela pourrait vous obliger à déclarer la méthode englobante comme faisant throws Exception . A partir de Java 7, le compilateur sait que l'ensemble des exceptions qui pourraient être (re-lancées) est plus petit.

Pitfall - Catching InterruptedException

Comme déjà souligné dans d’autres pièges, en rattrapant toutes les exceptions en utilisant

try {
    // Some code
} catch (Exception) {
    // Some error handling
}

Livré avec beaucoup de problèmes différents. Mais un problème particulier est qu’il peut entraîner des blocages lorsqu’il interrompt le système d’interruption lorsqu’il écrit des applications multithread.

Si vous lancez un thread, vous devez également pouvoir l'arrêter brusquement pour diverses raisons.

Thread t = new Thread(new Runnable() {
    public void run() {
         while (true) {
             //Do something indefinetely
         }
    }
}

t.start();

//Do something else

// The thread should be canceld if it is still active. 
// A Better way to solve this is with a shared variable that is tested 
// regularily by the thread for a clean exit, but for this example we try to 
// forcibly interrupt this thread.
if (t.isAlive()) {
   t.interrupt();
   t.join();
}

//Continue with program

Le t.interrupt() InterruptedException dans ce thread, car il est destiné à arrêter le thread. Mais que se passe-t-il si le thread doit nettoyer certaines ressources avant qu'il ne soit complètement arrêté? Pour cela, il peut intercepter l'exception InterruptedException et effectuer un nettoyage.

 Thread t = new Thread(new Runnable() {
    public void run() {
        try {
            while (true) {
                //Do something indefinetely
            }
        } catch (InterruptedException ex) {
            //Do some quick cleanup

            // In this case a simple return would do. 
            // But if you are not 100% sure that the thread ends after 
            // catching the InterruptedException you will need to raise another 
            // one for the layers surrounding this code.                
            Thread.currentThread().interrupt(); 
        }
    }
}

Mais si vous avez une expression catch-all dans votre code, l'exception InterruptedException sera également prise en compte et l'interruption ne se poursuivra pas. Ce qui dans ce cas pourrait conduire à un blocage car le thread parent attend indéfiniment que ce thead s'arrête avec t.join() .

 Thread t = new Thread(new Runnable() {
    public void run() {
        try {
            while (true) {
                try {
                    //Do something indefinetely
                }
                catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        } catch (InterruptedException ex) {
            // Dead code as the interrupt exception was already caught in
            // the inner try-catch           
            Thread.currentThread().interrupt(); 
        }
    }
}

Il est donc préférable d'attraper les exceptions individuellement, mais si vous insistez pour utiliser un catch-all, prenez au moins l'interception d'InterruptedException au préalable.

Thread t = new Thread(new Runnable() {
    public void run() {
        try {
            while (true) {
                try {
                    //Do something indefinetely
                } catch (InterruptedException ex) {
                    throw ex; //Send it up in the chain
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        } catch (InterruptedException ex) {
            // Some quick cleanup code 
    
            Thread.currentThread().interrupt(); 
        }
    }
}

Piège - Utilisation des exceptions pour un contrôle de flux normal

Il y a un mantra que certains experts Java récitent:

"Les exceptions ne doivent être utilisées que dans des cas exceptionnels."

(Par exemple: http://programmers.stackexchange.com/questions/184654 )

L'essentiel est que c'est une mauvaise idée (en Java) d'utiliser la gestion des exceptions et des exceptions pour implémenter un contrôle de flux normal. Par exemple, comparez ces deux manières de traiter un paramètre qui pourrait être nul.

public String truncateWordOrNull(String word, int maxLength) {
    if (word == null) {
        return "";
    } else {
        return word.substring(0, Math.min(word.length(), maxLength));
    }
}

public String truncateWordOrNull(String word, int maxLength) {
    try {
        return word.substring(0, Math.min(word.length(), maxLength));
    } catch (NullPointerException ex) {
        return "";
    }
}

Dans cet exemple, nous traitons (par conception) le cas où le word est null comme s'il s'agissait d'un mot vide. Les deux versions traitent de null soit en utilisant if ... else et ou try ... catch . Comment devrions-nous décider quelle version est la meilleure?

Le premier critère est la lisibilité. Bien que la lisibilité soit difficile à quantifier objectivement, la plupart des programmeurs conviendraient que la signification essentielle de la première version est plus facile à discerner. En effet, pour bien comprendre la seconde forme, vous devez comprendre qu'une NullPointerException ne peut pas être lancée par les méthodes Math.min ou String.substring .

Le deuxième critère est l'efficacité. Dans les versions de Java antérieures à Java 8, la deuxième version est significativement plus lente que la première version. En particulier, la construction d'un objet d'exception implique la capture et l'enregistrement des stackframes, au cas où la trace de pile serait requise.

D'autre part, il existe de nombreuses situations où l'utilisation des exceptions est plus lisible, plus efficace et (parfois) plus correcte que l'utilisation d'un code conditionnel pour gérer des événements "exceptionnels". En effet, il existe de rares situations où il est nécessaire de les utiliser pour des événements "non exceptionnels"; c'est-à-dire des événements relativement fréquents. Pour ces derniers, il convient de chercher des moyens de réduire les frais généraux liés à la création d’objets d’exception.

Piège - Empilement excessif ou inapproprié

Une des choses les plus ennuyeuses que les programmeurs puissent faire est de disperser les appels à printStackTrace() dans tout leur code.

Le problème est que printStackTrace() va écrire le stacktrace sur la sortie standard.

  • Pour une application destinée aux utilisateurs finaux qui ne sont pas des programmeurs Java, un stacktrace est, au mieux, non informatif et au pire alarmant.

  • Pour une application côté serveur, il y a des chances que personne ne regarde la sortie standard.

Une meilleure idée est de ne pas appeler directement printStackTrace ou, si vous l'appelez, de le faire de manière à ce que la trace de la pile soit écrite dans un fichier journal ou un fichier d'erreur plutôt que sur la console de l'utilisateur final.

Pour ce faire, vous pouvez utiliser une structure de journalisation et transmettre l'objet exception en tant que paramètre de l'événement de journal. Cependant, même l'enregistrement de l'exception peut être nuisible si elle est effectuée de manière abusive. Considérer ce qui suit:

public void method1() throws SomeException {
    try {
        method2();
        // Do something
    } catch (SomeException ex) {
        Logger.getLogger().warn("Something bad in method1", ex);
        throw ex;
    }
}

public void method2() throws SomeException {
    try {
        // Do something else
    } catch (SomeException ex) {
        Logger.getLogger().warn("Something bad in method2", ex);
        throw ex;
    }
}

Si l'exception est method2 dans method2 , vous êtes susceptible de voir deux copies de la même trace dans le fichier journal, correspondant au même échec.

En bref, consignez l'exception ou relancez-la (éventuellement avec une autre exception). Ne faites pas les deux.

Piège - Sous-classement direct «Throwable»

Throwable a deux sous-classes directes, Exception et Error . Bien qu'il soit possible de créer une nouvelle classe qui étend Throwable directement, cela est déconseillé, car de nombreuses applications supposent que seules les Exception et les Error existent.

Plus Throwable , il n'y a aucun avantage pratique à sous- Throwable directement Throwable , car la classe résultante est en fait simplement une exception vérifiée. L' Exception sous- Exception entraînera plutôt le même comportement, mais traduira plus clairement votre intention.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow