499 votes

Les arguments contre les exceptions vérifiées

Depuis plusieurs années, je ne parviens pas à obtenir une réponse décente à la question suivante : pourquoi certains développeurs sont-ils si opposés aux exceptions vérifiées ? J'ai eu de nombreuses conversations, j'ai lu des choses sur des blogs, j'ai lu ce que Bruce Eckel avait à dire (la première personne que j'ai vue s'exprimer contre elles).

Je suis en train d'écrire un nouveau code et je fais très attention à la façon dont je traite les exceptions. J'essaie de comprendre le point de vue de la foule "nous n'aimons pas les exceptions vérifiées" et je n'y arrive toujours pas.

Toutes les conversations que j'ai se terminent par la même question qui reste sans réponse... Laissez-moi vous expliquer :

En général (d'après la façon dont Java a été conçu),

  • Error est pour les choses qui ne devraient jamais être prises (VM est allergique aux cacahuètes et quelqu'un a fait tomber un pot de cacahuètes sur lui)
  • RuntimeException est pour les choses que le programmeur a mal faites (le programmeur a oublié la fin d'un tableau).
  • Exception (sauf RuntimeException ) est pour les choses qui sont hors du contrôle du programmeur (le disque se remplit lors de l'écriture dans le système de fichiers, la limite du gestionnaire de fichiers pour le processus a été atteinte et vous ne pouvez plus ouvrir de fichiers).
  • Throwable est simplement le parent de tous les types d'exception.

Un argument courant que j'entends est que si une exception se produit, tout ce que le développeur va faire est de quitter le programme.

Un autre argument courant que j'entends est que les exceptions vérifiées rendent plus difficile le remaniement du code.

Pour l'argument "tout ce que je vais faire est de quitter", je dis que même si vous quittez, vous devez afficher un message d'erreur raisonnable. Si vous vous contentez de gérer les erreurs, vos utilisateurs ne seront pas très heureux lorsque le programme quittera sans indication claire de la raison.

Pour ceux qui pensent que "cela rend difficile le remaniement", cela indique que le niveau d'abstraction approprié n'a pas été choisi. Plutôt que de déclarer qu'une méthode lance un IOException le IOException devrait être transformée en une exception plus adaptée à ce qui se passe.

Je n'ai pas de problème à envelopper Main avec catch(Exception) (ou dans certains cas catch(Throwable) afin de s'assurer que le programme peut se terminer de manière élégante - mais j'attrape toujours les exceptions spécifiques dont j'ai besoin. Cela me permet, au minimum, d'afficher un message d'erreur approprié.

La question à laquelle les gens ne répondent jamais est la suivante :

Si vous lancez RuntimeException au lieu de Exception des sous-classes, alors comment savoir ce que vous êtes censé attraper ?

Si la réponse est catch Exception alors vous traitez les erreurs du programmeur de la même manière que les exceptions du système. Cela me semble erroné.

Si vous attrapez Throwable alors vous traitez les exceptions système et les erreurs VM (et autres) de la même manière. Cela me semble erroné.

Si la réponse est que vous n'attrapez que les exceptions que vous savez être levées, comment savez-vous lesquelles ? Que se passe-t-il lorsque le programmeur X lance une nouvelle exception et oublie de l'attraper ? Cela me semble très dangereux.

Je dirais qu'un programme qui affiche une trace de la pile est mauvais. Les personnes qui n'aiment pas les exceptions vérifiées ne sont-elles pas de cet avis ?

Donc, si vous n'aimez pas les exceptions contrôlées, pouvez-vous expliquer pourquoi et répondre à la question qui n'a pas reçu de réponse ?

Je ne cherche pas à savoir quand utiliser l'un ou l'autre modèle, ce que je cherche c'est pourquoi les personnes s'étendent de RuntimeException parce qu'ils n'aiment pas s'étendre de Exception et/ou pourquoi ils attrapent une exception et relancent ensuite une RuntimeException plutôt que d'ajouter des lancers à leur méthode. Je veux comprendre les raisons pour lesquelles on n'aime pas les exceptions vérifiées.

47 votes

Je ne pense pas que ce soit complètement subjectif - c'est une fonctionnalité de la langue qui a été conçue pour avoir une utilisation spécifique, plutôt que pour que chacun puisse décider de ce à quoi elle sert pour lui-même. Et ce n'est pas spécialement argumentatif, ça répond à l'avance à des réfutations spécifiques que les gens auraient pu facilement trouver.

7 votes

Allez. Considéré comme une caractéristique de la langue, ce sujet a été et peut être abordé de manière objective.

6 votes

@cletus "répondre à votre propre question" si j'avais la réponse je n'aurais pas posé la question !

311voto

Rhubarb Points 16453

Je pense avoir lu la même interview de Bruce Eckel que vous - et cela m'a toujours dérangé. En fait, l'argument a été avancé par l'interviewé (si c'est bien le post dont vous parlez) Anders Hejlsberg, le génie de MS derrière .NET et C#.

http://www.artima.com/intv/handcuffs.html

Bien que je sois fan de Hejlsberg et de son travail, cet argument m'a toujours paru bidon. Il se résume essentiellement à :

"Les exceptions vérifiées sont mauvaises parce que les programmeurs en abusent en les attrapant toujours et en les rejetant, ce qui conduit à cacher et à ignorer des problèmes qui seraient autrement présentés à l'utilisateur".

Par "autrement présenté à l'utilisateur" Je veux dire que si vous utilisez une exception d'exécution, le programmeur paresseux l'ignorera (au lieu de l'attraper avec un bloc catch vide) et l'utilisateur la verra.

Le résumé de l'argument est le suivant "Les programmeurs ne les utiliseront pas correctement et ne pas les utiliser correctement est pire que de ne pas les avoir". .

Il y a une part de vérité dans cet argument et, en fait, je soupçonne que la motivation de Gosling pour ne pas mettre de surcharges d'opérateurs en Java vient d'un argument similaire - elles déroutent le programmeur parce qu'elles sont souvent mal utilisées.

Mais en fin de compte, je trouve que c'est un argument bidon de Hejlsberg et peut-être un argument post-hoc créé pour expliquer le manque plutôt qu'une décision bien réfléchie.

Je dirais que si la surutilisation des exceptions vérifiées est une mauvaise chose et tend à conduire à une gestion négligée par les utilisateurs, leur utilisation correcte permet au programmeur de l'API d'offrir de grands avantages au programmeur du client de l'API.

Désormais, le programmeur de l'API doit veiller à ne pas lancer des exceptions vérifiées partout, sinon elles ne feront qu'ennuyer le programmeur du client. Le programmeur client très paresseux aura recours à catch (Exception) {} comme le prévient Hejlsberg, tout bénéfice sera perdu et l'enfer s'ensuivra. Mais dans certaines circonstances, il n'y a pas de substitut à une bonne exception vérifiée.

Pour moi, l'exemple classique est l'API d'ouverture de fichiers. Chaque langage de programmation dans l'histoire des langages (sur les systèmes de fichiers au moins) possède une API quelque part qui vous permet d'ouvrir un fichier. Et chaque programmeur client utilisant cette API sait qu'il doit faire face au cas où le fichier qu'il essaie d'ouvrir n'existe pas. Permettez-moi de reformuler cette phrase : Chaque programmeur client qui utilise cette API devrait savoir qu'ils doivent s'occuper de cette affaire. Et c'est là que le bât blesse : le programmeur de l'API peut-il les aider à savoir qu'ils doivent traiter ce cas par le biais de commentaires uniquement ou peut-il en effet insister le client s'en occupe.

En C, l'idiome donne quelque chose comme

  if (f = fopen("goodluckfindingthisfile")) { ... } 
  else { // file not found ...

donde fopen indique l'échec en retournant 0 et C (bêtement) vous laisse traiter 0 comme un booléen et... En gros, vous apprenez cet idiome et tout va bien. Mais que faire si vous êtes un noob et que vous n'avez pas appris l'idiome. Alors, bien sûr, vous commencez avec

   f = fopen("goodluckfindingthisfile");
   f.read(); // BANG! 

et apprendre à la dure.

Notez que nous ne parlons ici que des langages fortement typés : Il y a une idée claire de ce qu'est une API dans un langage fortement typé : C'est un assortiment de fonctionnalités (méthodes) que vous pouvez utiliser avec un protocole clairement défini pour chacune d'entre elles.

Ce protocole clairement défini est généralement défini par une signature de méthode. Ici, fopen exige que vous lui passiez une chaîne de caractères (ou un char* dans le cas du C). Si vous lui fournissez autre chose, vous obtenez une erreur de compilation. Vous n'avez pas suivi le protocole - vous n'utilisez pas l'API correctement.

Dans certains langages (obscurs), le type de retour fait également partie du protocole. Si vous essayez d'appeler l'équivalent de fopen() dans certains langages, sans l'assigner à une variable, vous obtiendrez également une erreur de compilation (vous ne pouvez le faire qu'avec les fonctions void).

Ce que j'essaie de dire c'est que : Dans un langage à typage statique, le programmeur de l'API encourage le client à utiliser correctement l'API en empêchant la compilation de son code client s'il commet des erreurs évidentes.

(Dans un langage typé dynamiquement, comme Ruby, vous pouvez passer n'importe quoi, par exemple un flottant, comme nom de fichier - et il sera compilé. Pourquoi embêter l'utilisateur avec des exceptions vérifiées si vous ne contrôlez même pas les arguments de la méthode. Les arguments avancés ici ne s'appliquent qu'aux langages à typage statique).

Alors, qu'en est-il des exceptions contrôlées ?

Voici l'une des API Java que vous pouvez utiliser pour ouvrir un fichier.

try {
  f = new FileInputStream("goodluckfindingthisfile");
}
catch (FileNotFoundException e) {
  // deal with it. No really, deal with it!
  ... // this is me dealing with it
}

Tu vois cette prise ? Voici la signature de cette méthode API :

public FileInputStream(String name)
                throws FileNotFoundException

Notez que FileNotFoundException est un vérifié exception.

C'est le programmeur de l'API qui vous le dit : " Vous pouvez utiliser ce constructeur pour créer un nouveau FileInputStream mais vous

a) doit passe dans le nom du fichier comme un chaîne de caractères
b) doit accepter le la possibilité que le fichier ne soit pas être trouvé au moment de l'exécution"

Et c'est toute la question, en ce qui me concerne.

La clé est essentiellement ce que la question énonce comme "Les choses qui sont hors du contrôle du programmeur". Ma première pensée était qu'il/elle voulait dire les choses qui sont hors de la API le contrôle des programmeurs. Mais en fait, les exceptions contrôlées, lorsqu'elles sont utilisées correctement, doivent être réservées à des choses qui échappent au contrôle du programmeur du client et du programmeur de l'API. Je pense que c'est la clé pour ne pas abuser des exceptions contrôlées.

Je pense que l'ouverture du fichier illustre bien le propos. Le programmeur de l'API sait que vous pouvez lui donner un nom de fichier qui s'avère inexistant au moment où l'API est appelée, et qu'il ne pourra pas vous renvoyer ce que vous vouliez, mais qu'il devra lever une exception. Il sait également que cela se produira assez régulièrement et que le programmeur client peut s'attendre à ce que le nom de fichier soit correct au moment où il a écrit l'appel, mais qu'il peut aussi être erroné au moment de l'exécution pour des raisons indépendantes de sa volonté.

L'API le rend donc explicite : Il y aura des cas où ce fichier n'existe pas au moment où vous m'appelez et vous feriez bien de vous en occuper.

Cela serait plus clair avec un contre cas. Imaginez que j'écrive une API de table. J'ai le modèle de table quelque part avec une API comprenant cette méthode :

public RowData getRowData(int row) 

En tant que programmeur d'API, je sais qu'il y aura des cas où un client transmettra une valeur négative pour la ligne ou une valeur de ligne en dehors du tableau. Je pourrais donc être tenté de lancer une exception vérifiée et de forcer le client à s'en occuper :

public RowData getRowData(int row) throws CheckedInvalidRowNumberException

(Je n'appellerais pas vraiment ça "Vérifié", bien sûr.)

C'est une mauvaise utilisation des exceptions vérifiées. Le code client va être rempli d'appels pour récupérer des données de ligne, chacun d'entre eux devant utiliser un try/catch, et pour quoi faire ? Vont-ils signaler à l'utilisateur que la mauvaise ligne a été recherchée ? Probablement pas, car quelle que soit l'interface utilisateur qui entoure ma vue de table, elle ne devrait pas laisser l'utilisateur se mettre dans un état où une ligne illégale est demandée. Il s'agit donc d'un bug de la part du programmeur du client.

Le programmeur de l'API peut toujours prévoir que le client codera de tels bogues et devrait les traiter avec une exception d'exécution comme une erreur d'exécution. IllegalArgumentException .

Avec une exception vérifiée dans getRowData il s'agit clairement d'un cas qui conduira le programmeur paresseux de Hejlsberg à ajouter simplement des prises vides. Dans ce cas, les valeurs de ligne illégales ne seront pas évidentes, même pour le testeur ou le développeur client qui débogue, mais elles entraîneront des erreurs en chaîne dont il sera difficile de déterminer la source. Les fusées Arianne explosent après le lancement.

Ok, alors voilà le problème : je dis que l'exception vérifiée FileNotFoundException n'est pas seulement une bonne chose mais un outil essentiel dans la boîte à outils des programmeurs d'API pour définir l'API de la manière la plus utile pour le programmeur client. Mais le CheckedInvalidRowNumberException est un gros inconvénient qui conduit à une mauvaise programmation et doit être évité. Mais comment faire la différence ?

Je suppose que ce n'est pas une science exacte et je suppose que cela sous-tend et peut-être justifie dans une certaine mesure l'argument de Hejlsberg. Mais je n'aime pas jeter le bébé avec l'eau du bain, alors permettez-moi de dégager quelques règles pour distinguer les bonnes exceptions contrôlées des mauvaises :

  1. Hors du contrôle du client ou fermé vs ouvert :

    Les exceptions vérifiées ne doivent être utilisées que lorsque le cas d'erreur échappe au contrôle de l'API. y le programmeur du client. Cela a à voir avec la façon dont ouvrir ou fermé le système est. Dans un contraint Dans une interface utilisateur où le programmeur client a le contrôle, par exemple, de tous les boutons, des commandes clavier, etc. qui ajoutent et suppriment des lignes dans la vue tableau (un système fermé), il s'agit d'un bogue de programmation client s'il tente d'extraire des données d'une ligne inexistante. Dans un système d'exploitation basé sur des fichiers où un nombre quelconque d'utilisateurs/applications peuvent ajouter et supprimer des fichiers (un système ouvert), il est concevable que le fichier demandé par le client ait été supprimé à son insu et qu'il doive donc s'en occuper.

  2. Ubiquité :

    Les exceptions vérifiées ne doivent pas être utilisées sur un appel d'API qui est effectué fréquemment par le client. Par fréquemment, j'entends à partir de nombreux endroits du code client, et non pas fréquemment dans le temps. Ainsi, un code client n'a pas tendance à essayer d'ouvrir le même fichier à plusieurs reprises, mais ma vue de la table reçoit des exceptions. RowData partout, à partir de différentes méthodes. En particulier, je vais écrire beaucoup de code du type

    if (model.getRowData().getCell(0).isEmpty())

et il sera douloureux de devoir envelopper dans try/catch à chaque fois.

  1. Informer l'utilisateur :

    Les exceptions vérifiées doivent être utilisées dans les cas où vous pouvez imaginer qu'un message d'erreur utile soit présenté à l'utilisateur final. Il s'agit de l'exception "et que ferez-vous quand ça arrivera ?" question que j'ai soulevée ci-dessus. Elle est également liée au point 1. Puisque vous pouvez prévoir que quelque chose d'extérieur à votre système client-API pourrait entraîner l'absence du fichier, vous pouvez raisonnablement en informer l'utilisateur :

    "Error: could not find the file 'goodluckfindingthisfile'"

    Étant donné que votre numéro de ligne illégal a été causé par un bogue interne et sans que l'utilisateur en soit responsable, il n'y a vraiment aucune information utile que vous puissiez leur donner. Si votre application ne permet pas aux exceptions d'exécution de se répercuter sur la console, elle finira probablement par leur donner un message affreux du genre :

    "Internal error occured: IllegalArgumentException in ...."

    En bref, si vous pensez que le programmeur de votre client n'est pas en mesure d'expliquer votre exception de manière à aider l'utilisateur, vous ne devriez probablement pas utiliser d'exception vérifiée.

Ce sont donc mes règles. Elles sont quelque peu inventées, et il y aura sans doute des exceptions (aidez-moi à les affiner si vous le souhaitez). Mais mon argument principal est qu'il existe des cas tels que FileNotFoundException où l'exception vérifiée est une partie du contrat de l'API aussi importante et utile que les types de paramètres. Nous ne devrions donc pas nous en passer simplement parce qu'elle est mal utilisée.

Désolé, je ne voulais pas que ce soit aussi long et verbeux. Permettez-moi de terminer par deux suggestions :

R : programmeurs d'API : utilisez les exceptions vérifiées avec parcimonie pour préserver leur utilité. En cas de doute, utilisez une exception non vérifiée.

B : Programmeurs clients : prenez l'habitude de créer une exception enveloppée (googlez-le) dès le début de votre développement. Le JDK 1.4 et les versions ultérieures fournissent un constructeur dans la section RuntimeException pour cela, mais vous pouvez facilement créer le vôtre aussi. Voici le constructeur :

public RuntimeException(Throwable cause)

Ensuite, prenez l'habitude, chaque fois que vous devez gérer une exception vérifiée et que vous vous sentez paresseux (ou que vous pensez que le programmeur de l'API a fait un excès de zèle en utilisant l'exception vérifiée en premier lieu), de ne pas simplement avaler l'exception, de l'envelopper et de la relancer.

try {
  overzealousAPI(thisArgumentWontWork);
}
catch (OverzealousCheckedException exception) {
  throw new RuntimeException(exception);  
}

Mettez-le dans l'un des petits modèles de code de votre IDE et utilisez-le lorsque vous vous sentez paresseux. De cette façon, si vous avez vraiment besoin de gérer l'exception vérifiée, vous serez obligé de revenir et de vous en occuper après avoir vu le problème au moment de l'exécution. Parce que, croyez-moi (et Anders Hejlsberg), vous ne reviendrez jamais à ce TODO dans votre fichier

catch (Exception e) { /* TODO deal with this at some point (yeah right) */}

207voto

bobince Points 270740

Le problème des exceptions vérifiées est qu'elles ne sont pas vraiment des exceptions au sens habituel du concept. Il s'agit plutôt de valeurs de retour alternatives de l'API.

L'idée générale des exceptions est qu'une erreur lancée quelque part en bas de la chaîne d'appel peut remonter et être traitée par le code situé plus haut, sans que le code intermédiaire ait à s'en préoccuper. Les exceptions vérifiées, en revanche, exigent que chaque niveau de code entre le lanceur et le receveur déclare connaître toutes les formes d'exception qui peuvent les traverser. En pratique, cela n'est guère différent de ce qui se passerait si les exceptions vérifiées étaient simplement des valeurs de retour spéciales que l'appelant doit vérifier. par exemple, [pseudocode] :

public [int or IOException] writeToStream(OutputStream stream) {
    [void or IOException] a= stream.write(mybytes);
    if (a instanceof IOException)
        return a;
    return mybytes.length;
}

Puisque Java ne peut pas faire de valeurs de retour alternatives, ou de simples tuples en ligne comme valeurs de retour, les exceptions vérifiées sont une réponse raisonnable.

Le problème est qu'une grande partie du code, y compris une grande partie de la bibliothèque standard, utilise abusivement les exceptions vérifiées pour des conditions exceptionnelles réelles que vous pourriez très bien vouloir attraper plusieurs niveaux au-dessus. Pourquoi IOException n'est-elle pas une RuntimeException ? Dans tous les autres langages, je peux laisser une exception IO se produire, et si je ne fais rien pour la gérer, mon application s'arrêtera et j'obtiendrai une trace de pile pratique à consulter. C'est la meilleure chose qui puisse arriver.

Peut-être que deux méthodes plus haut dans l'exemple, vous voulez attraper toutes les IOExceptions de l'ensemble du processus d'écriture en flux, interrompre le processus et passer au code de rapport d'erreur ; en Java, vous ne pouvez pas le faire sans ajouter "throws IOException" à chaque niveau d'appel, même aux niveaux qui ne font pas d'IO. Ces méthodes ne devraient pas avoir besoin de connaître la gestion des exceptions ; elles doivent ajouter des exceptions à leur signature :

  1. augmente inutilement le couplage ;
  2. rend les signatures d'interface très fragiles au changement ;
  3. rend le code moins lisible ;
  4. est si ennuyeux que la réaction habituelle des programmeurs est de déjouer le système en faisant quelque chose d'horrible comme "throws Exception", "catch (Exception e) {}", ou en enveloppant tout dans une RuntimeException (ce qui rend le débogage plus difficile).

Et puis il y a plein d'exceptions de bibliothèque tout simplement ridicules comme :

try {
    httpconn.setRequestMethod("POST");
} catch (ProtocolException e) {
    throw new CanNeverHappenException("oh dear!");
}

Lorsqu'il faut encombrer son code avec de telles absurdités, il n'est pas étonnant que les exceptions vérifiées soient détestées, même s'il s'agit simplement d'une mauvaise conception de l'API.

Un autre effet particulièrement néfaste concerne l'inversion de contrôle, lorsque le composant A fournit une fonction de rappel au composant générique B. Le composant A veut être en mesure de laisser une exception se produire à partir de sa fonction de rappel et de la renvoyer à l'endroit où il a appelé le composant B, mais il ne peut pas le faire parce que cela modifierait l'interface de la fonction de rappel qui est fixée par B. A ne peut le faire qu'en enveloppant l'exception réelle dans une RuntimeException, ce qui représente encore plus de texte passe-partout de gestion des exceptions à écrire.

Les exceptions vérifiées, telles qu'elles sont implémentées dans Java et sa bibliothèque standard, sont de la bouillie, de la bouillie et de la bouillie. Dans un langage déjà verbeux, ce n'est pas une victoire.

15 votes

Dans votre exemple de code, il serait préférable de chaîner les exceptions afin de pouvoir retrouver la cause initiale en lisant les journaux : throw CanNeverHappenException(e) ;

5 votes

Je ne suis pas d'accord. Les exceptions, vérifiées ou non, sont des conditions exceptionnelles. Exemple : une méthode qui récupère un objet via HTTP. La valeur de retour est l'objet ou rien, toutes les choses qui peuvent mal tourner sont exceptionnelles. Les traiter comme des valeurs de retour, comme cela se fait en C, ne mène qu'à la confusion et à une mauvaise conception.

18 votes

@Monsieur : Ce que je veux dire, c'est que les exceptions vérifiées telles qu'elles sont implémentées en Java se comportent, en pratique, davantage comme des valeurs de retour comme en C que comme les "exceptions" traditionnelles que nous pouvons reconnaître dans le C++ et d'autres langages pré-Java. Et que, selon l'OMI, cela entraîne effectivement une confusion et une mauvaise conception.

83voto

cletus Points 276888

Plutôt que de répéter toutes les (nombreuses) raisons qui militent contre les exceptions vérifiées, je vais en choisir une seule. J'ai perdu le compte du nombre de fois où j'ai écrit ce bloc de code :

try {
  // do stuff
} catch (AnnoyingcheckedException e) {
  throw new RuntimeException(e);
}

99% du temps, je ne peux rien y faire. Enfin, les blocs font tout le nettoyage nécessaire (ou du moins ils devraient).

J'ai aussi perdu le compte du nombre de fois où j'ai vu ça :

try {
  // do stuff
} catch (AnnoyingCheckedException e) {
  // do nothing
}

Pourquoi ? Parce que quelqu'un devait s'en occuper et était paresseux. C'était mal ? Bien sûr. Ça arrive ? Absolument. Et si c'était une exception non vérifiée à la place ? L'application serait simplement morte (ce qui est préférable que d'avaler une exception).

Et puis, nous avons du code exaspérant qui utilise les exceptions comme une forme de contrôle de flux, comme java.text.Format fait. Bzzzt. Faux. Un utilisateur qui met "abc" dans un champ numérique sur un formulaire n'est pas une exception.

Ok, je suppose que ça fait trois raisons.

4 votes

Mais si une exception est correctement détectée, vous pouvez informer l'utilisateur, effectuer d'autres tâches (journalisation ?) et quitter l'application de manière contrôlée. Je suis d'accord que certaines parties de l'API auraient pu être mieux conçues. Et pour la raison du programmeur paresseux, eh bien, je pense qu'en tant que programmeur vous êtes 100% responsable de votre code.

3 votes

Notez que le try-catch-rethrow vous permet de spécifier un message - je l'utilise généralement pour ajouter des informations sur le contenu des variables d'état. Un exemple fréquent est celui des IOExceptions pour ajouter le absolutePathName() du fichier en question.

0 votes

Je pense que vous avez identifié le plus gros problème avec les exceptions vérifiées ; si foo appelle bar y bar peut, de manière inattendue, lancer un wazooException foo n'est pas prête à gérer, il devrait y avoir un moyen facile pour foo de déclarer simplement qu'elle n'est pas prête à gérer une wazooException de bar . Passing it up via throws est le mauvais puisque l'appelant ne sera pas en mesure de faire la distinction entre une wazooException jeté par foo pour des raisons foo anticipé, et un wazooException jeté par bar pour des raisons foo ne s'y attendait pas.

64voto

Boann Points 11904

Je sais qu'il s'agit d'une vieille question, mais j'ai passé un certain temps à me battre avec des exceptions vérifiées et j'ai quelque chose à ajouter. Je vous prie de m'excuser pour sa longueur !

Mon principal problème avec les exceptions vérifiées est qu'elles détruisent le polymorphisme. Il est impossible de les faire jouer gentiment avec les interfaces polymorphes.

Prenez la bonne vieille Java List interface. Nous avons des implémentations courantes en mémoire comme ArrayList y LinkedList . Nous avons aussi la classe squelettique AbstractList ce qui permet de concevoir facilement de nouveaux types de listes. Pour une liste en lecture seule, nous ne devons implémenter que deux méthodes : size() y get(int index) .

Cet exemple WidgetList La classe lit certains objets de taille fixe de type Widget (non montré) à partir d'un fichier :

class WidgetList extends AbstractList<Widget> {
    private static final int SIZE_OF_WIDGET = 100;
    private final RandomAccessFile file;

    public WidgetList(RandomAccessFile file) {
        this.file = file;
    }

    @Override
    public int size() {
        return (int)(file.length() / SIZE_OF_WIDGET);
    }

    @Override
    public Widget get(int index) {
        file.seek((long)index * SIZE_OF_WIDGET);
        byte[] data = new byte[SIZE_OF_WIDGET];
        file.read(data);
        return new Widget(data);
    }
}

En exposant les widgets à l'aide de l'outil familier List vous pouvez récupérer des éléments ( list.get(123) ) ou itérer une liste ( for (Widget w : list) ... ) sans avoir besoin de connaître WidgetList même. On peut passer cette liste à n'importe quelle méthode standard qui utilise des listes génériques, ou l'envelopper dans une balise Collections.synchronizedList . Le code qui l'utilise n'a pas besoin de savoir ni de se soucier de savoir si les "widgets" sont créés sur place, s'ils proviennent d'un tableau, s'ils sont lus à partir d'un fichier, d'une base de données, d'un réseau ou d'un futur relais subspatial. Il fonctionnera toujours correctement parce que le List est correctement implémentée.

Sauf que ça ne l'est pas. La classe ci-dessus ne compile pas parce que les méthodes d'accès aux fichiers peuvent lancer une erreur de type IOException une exception vérifiée que vous devez "attraper ou spécifier". Vous ne peut pas le spécifier comme jeté -- le compilateur ne vous laissera pas faire parce que cela violerait le contrat du List l'interface. Et il n'y a aucun moyen utile pour que WidgetList peut gérer l'exception (comme je l'expliquerai plus tard).

Apparemment, la seule chose à faire est d'attraper et de relancer les exceptions vérifiées comme des exceptions non vérifiées :

@Override
public int size() {
    try {
        return (int)(file.length() / SIZE_OF_WIDGET);
    } catch (IOException e) {
        throw new WidgetListException(e);
    }
}

public static class WidgetListException extends RuntimeException {
    public WidgetListException(Throwable cause) {
        super(cause);
    }
}

((Edit : Java 8 a ajouté un UncheckedIOException pour exactement ce cas : pour attraper et relancer IOException à travers les frontières des méthodes polymorphes. Cela prouve en quelque sorte mon point de vue !))

Les exceptions ainsi vérifiées ne fonctionnent tout simplement pas dans des cas comme celui-ci. Vous ne pouvez pas les jeter. Idem pour un astucieux Map soutenu par une base de données, ou une implémentation de java.util.Random connecté à une source d'entropie quantique via un port COM. Dès que vous essayez de faire quelque chose de nouveau avec l'implémentation d'une interface polymorphe, le concept d'exceptions vérifiées échoue. Mais les exceptions vérifiées sont si insidieuses qu'elles ne vous laissent pas en paix, car vous devez toujours les attraper et les relancer à partir de méthodes de niveau inférieur, ce qui encombre le code et la trace de la pile.

Je trouve que l'omniprésente Runnable est souvent coincée dans ce coin, si elle appelle quelque chose qui lance des exceptions vérifiées. Elle ne peut pas lancer l'exception telle quelle, et tout ce qu'elle peut faire, c'est encombrer le code en l'attrapant et en la relançant en tant qu'exception vérifiée. RuntimeException .

En fait, vous peut lancer des exceptions contrôlées non déclarées si vous avez recours à des bidouillages. La JVM, au moment de l'exécution, ne se soucie pas des règles d'exceptions vérifiées, nous devons donc tromper uniquement le compilateur. La façon la plus simple de le faire est d'abuser des génériques. Voici ma méthode (le nom de la classe est indiqué car (avant Java 8) il est requis dans la syntaxe d'appel de la méthode générique) :

class Util {
    /**
     * Throws any {@link Throwable} without needing to declare it in the
     * method's {@code throws} clause.
     * 
     * <p>When calling, it is suggested to prepend this method by the
     * {@code throw} keyword. This tells the compiler about the control flow,
     * about reachable and unreachable code. (For example, you don't need to
     * specify a method return value when throwing an exception.) To support
     * this, this method has a return type of {@link RuntimeException},
     * although it never returns anything.
     * 
     * @param t the {@code Throwable} to throw
     * @return nothing; this method never returns normally
     * @throws Throwable that was provided to the method
     * @throws NullPointerException if {@code t} is {@code null}
     */
    public static RuntimeException sneakyThrow(Throwable t) {
        return Util.<RuntimeException>sneakyThrow1(t);
    }

    @SuppressWarnings("unchecked")
    private static <T extends Throwable> RuntimeException sneakyThrow1(
            Throwable t) throws T {
        throw (T)t;
    }
}

Hourra ! En utilisant ceci, nous pouvons lancer une exception vérifiée à n'importe quelle profondeur de la pile sans la déclarer, sans l'envelopper dans un fichier RuntimeException et sans encombrer la trace de la pile ! Reprenons l'exemple de "WidgetList" :

@Override
public int size() {
    try {
        return (int)(file.length() / SIZE_OF_WIDGET);
    } catch (IOException e) {
        throw sneakyThrow(e);
    }
}

Malheureusement, l'insulte finale des exceptions contrôlées est que le compilateur refuse de vous permettre de attraper une exception vérifiée si, selon son opinion erronée, elle n'aurait pas pu être levée. (Les exceptions non vérifiées n'ont pas cette règle.) Pour attraper l'exception sournoisement lancée, nous devons faire ceci :

try {
    ...
} catch (Throwable t) { // catch everything
    if (t instanceof IOException) {
        // handle it
        ...
    } else {
        // didn't want to catch this one; let it go
        throw t;
    }
}

C'est un peu maladroit, mais d'un autre côté, c'est toujours un peu plus simple que le code permettant d'extraire une exception vérifiée qui était enveloppée dans une balise RuntimeException .

Heureusement, le throw t; est légal ici, même si le type d'instruction t est vérifiée, grâce à une règle ajoutée dans Java 7 concernant le rejet des exceptions rattrapées.


Lorsque les exceptions vérifiées rencontrent le polymorphisme, le cas inverse est également un problème : lorsqu'une méthode est spécifiée comme pouvant potentiellement lancer une exception vérifiée, mais qu'une implémentation surchargée ne le fait pas. Par exemple, la classe abstraite OutputStream 's write Toutes les méthodes spécifient throws IOException . ByteArrayOutputStream est une sous-classe qui écrit dans un tableau en mémoire au lieu d'une véritable source d'entrée/sortie. Sa surcharge write ne peuvent pas provoquer IOException donc ils n'ont pas de throws et vous pouvez les appeler sans vous soucier de l'exigence catch-or-specify.

Sauf que pas toujours. Supposons que Widget a une méthode pour l'enregistrer dans un flux :

public void writeTo(OutputStream out) throws IOException;

Déclarer cette méthode pour accepter un simple OutputStream est la bonne chose à faire, de sorte qu'il peut être utilisé de manière polymorphe avec toutes sortes de sorties : fichiers, bases de données, le réseau, et ainsi de suite. Et les tableaux en mémoire. Avec un tableau en mémoire, cependant, il y a une fausse exigence pour gérer une exception qui ne peut pas réellement se produire :

ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
    someWidget.writeTo(out);
} catch (IOException e) {
    // can't happen (although we shouldn't ignore it if it does)
    throw new RuntimeException(e);
}

Comme d'habitude, des exceptions contrôlées se mettent en travers du chemin. Si vos variables sont déclarées comme un type de base qui a des exigences d'exception plus ouvertes, vous devez ajouter des gestionnaires pour ces exceptions même si vous connaître ils ne se produiront pas dans votre application.

Mais attendez, les exceptions vérifiées sont en fait donc ennuyeux, que ils ne vous laissent même pas faire l'inverse ! Imaginez que vous attrapez actuellement IOException jeté par write fait appel à un OutputStream mais vous voulez changer le type déclaré de la variable en une variable ByteArrayOutputStream Le compilateur vous reprochera d'essayer d'attraper une exception vérifiée qui, selon lui, ne peut pas être levée.

Cette règle pose des problèmes absurdes. Par exemple, l'un des trois write méthodes de OutputStream es no remplacée par ByteArrayOutputStream . Plus précisément, write(byte[] data) est une méthode de commodité qui écrit le tableau complet en appelant write(byte[] data, int offset, int length) avec un décalage de 0 et la longueur du tableau. ByteArrayOutputStream remplace la méthode à trois arguments mais hérite de la méthode de commodité à un argument telle quelle. La méthode héritée fait exactement ce qu'il faut, mais elle inclut un élément non désiré throws clause. C'était peut-être un oubli dans la conception de ByteArrayOutputStream mais ils ne peuvent jamais la corriger car cela romprait la compatibilité avec tout code qui attrape l'exception - l'exception qui n'a jamais été, n'est jamais et ne sera jamais levée !

Cette règle est également gênante pendant l'édition et le débogage. Par exemple, il m'arrive de commenter temporairement un appel de méthode, et s'il aurait pu déclencher une exception vérifiée, le compilateur se plaindra maintenant de l'existence de la variable locale try y catch blocs. Je dois donc les commenter également, et maintenant, lorsque je modifie le code à l'intérieur, l'IDE s'indentera au mauvais niveau parce que l'élément { y } sont commentés. Gah ! C'est une petite plainte mais il semble que la seule chose que les exceptions contrôlées fassent est de causer des problèmes.


J'ai presque fini. Ma dernière frustration avec les exceptions vérifiées est que dans la plupart des sites d'appel il n'y a rien d'utile que tu puisses faire avec eux. Idéalement, lorsque quelque chose ne va pas, nous aurions un gestionnaire compétent, spécifique à l'application, qui pourrait informer l'utilisateur du problème et/ou terminer ou réessayer l'opération, selon le cas. Seul un gestionnaire situé en haut de la pile peut le faire, car il est le seul à connaître l'objectif global.

Au lieu de cela, nous avons l'idiome suivant, qui est très répandu comme moyen de faire taire le compilateur :

try {
    ...
} catch (SomeStupidExceptionOmgWhoCares e) {
    e.printStackTrace();
}

Dans une interface graphique ou un programme automatisé, le message imprimé ne sera pas vu. Pire encore, le reste du code se poursuit après l'exception. L'exception n'est pas vraiment une erreur ? Alors ne l'imprimez pas. Sinon, quelque chose d'autre va exploser dans un instant, et à ce moment-là, l'objet d'exception original aura disparu. Cet idiome n'est pas meilleur que le langage BASIC On Error Resume Next ou de PHP error_reporting(0); .

L'appel à une sorte de classe de journalisation n'est pas beaucoup mieux :

try {
    ...
} catch (SomethingWeird e) {
    logger.log(e);
}

C'est tout aussi paresseux que e.printStackTrace(); et continue à faire avancer le code dans un état indéterminé. De plus, le choix d'un système de journalisation particulier ou d'un autre gestionnaire est spécifique à l'application, ce qui nuit à la réutilisation du code.

Mais attendez ! Il existe un moyen simple et universel de trouver le gestionnaire spécifique à l'application. Il se trouve plus haut dans la pile d'appels (ou il est défini comme le gestionnaire de l'application du Thread). Gestionnaire d'exception non attrapée ). Donc, dans la plupart des endroits, tout ce que vous devez faire est de lancer l'exception plus haut dans la pile. . Par exemple, throw e; . Les exceptions vérifiées ne font qu'entraver le processus.

Je suis sûr que les exceptions contrôlées semblaient être une bonne idée lorsque le langage a été conçu, mais dans la pratique, j'ai constaté qu'elles ne présentaient que des inconvénients et aucun avantage.

47voto

Richard Levasseur Points 5428

Il ne s'agit pas d'afficher une trace de pile ou de se planter silencieusement. Il s'agit de pouvoir communiquer les erreurs entre les couches.

Le problème des exceptions vérifiées est qu'elles encouragent les gens à avaler des détails importants (à savoir, la classe d'exception). Si vous choisissez de ne pas avaler ce détail, alors vous devez continuer à ajouter des déclarations throws dans toute votre application. Cela signifie 1) qu'un nouveau type d'exception affectera de nombreuses signatures de fonctions, et 2) que vous pouvez manquer une instance spécifique de l'exception que vous voulez réellement attraper (disons que vous ouvrez un fichier secondaire pour une fonction qui écrit des données dans un fichier. Le fichier secondaire est facultatif, vous pouvez donc ignorer ses erreurs, mais parce que la signature throws IOException il est facile de ne pas en tenir compte).

Je suis actuellement confronté à cette situation dans une application. Nous avons reconditionné presque toutes les exceptions en AppSpecificException. Cela a rendu les signatures vraiment propres et nous n'avions pas à nous inquiéter de l'explosion throws dans les signatures.

Bien entendu, nous devons maintenant spécialiser la gestion des erreurs aux niveaux supérieurs, en mettant en œuvre une logique de relance, etc. Tout est AppSpecificException, cependant, donc nous ne pouvons pas dire "Si une IOException est levée, réessayer" ou "Si ClassNotFound est levée, abandonner complètement". Nous ne disposons pas d'un moyen fiable d'accéder à l'élément réel exception parce que les choses sont repackagées encore et encore lorsqu'elles passent entre notre code et celui d'un tiers.

C'est pourquoi je suis un grand fan de la gestion des exceptions en Python. Vous ne pouvez attraper que les choses que vous voulez et/ou pouvez gérer. Tout le reste se transforme en bulles comme si vous l'aviez relancé vous-même (ce que vous avez fait de toute façon).

J'ai constaté, à maintes reprises, et tout au long du projet que j'ai mentionné, que la gestion des exceptions se divise en 3 catégories :

  1. Attrapez et traitez un spécifique exception. Cela permet de mettre en œuvre une logique de relance, par exemple.
  2. Attraper et relancer autre exceptions. Tout ce qui se passe ici est généralement la journalisation, et c'est généralement un message banal comme "Impossible d'ouvrir $filename". Il s'agit d'erreurs auxquelles vous ne pouvez rien faire ; seul un niveau supérieur en sait assez pour les gérer.
  3. Attrape tout et affiche un message d'erreur. Ceci se trouve généralement à la racine d'un distributeur, et tout ce qu'il fait, c'est s'assurer qu'il peut communiquer l'erreur à l'appelant via un mécanisme sans exception (dialogue popup, marshaling d'un objet RPC Error, etc).

5 votes

Vous auriez pu créer des sous-classes spécifiques de AppSpecificException pour permettre la séparation tout en conservant les signatures de méthodes simples.

1 votes

Un autre ajout très important au point 2 est qu'il vous permet d'AJOUTER DES INFORMATIONS à l'exception capturée (par exemple en l'imbriquant dans une RuntimeException). Il est beaucoup, beaucoup mieux d'avoir le nom du fichier non trouvé dans la trace de la pile, que caché profondément dans un fichier journal.

1 votes

En gros, votre argument est le suivant : "Gérer les exceptions est fatigant, je préfère ne pas m'en occuper". Au fur et à mesure que l'exception s'accumule, elle perd son sens et la mise en contexte est pratiquement inutile. Si mon programme se plante parce que je n'ai pas été informé que telle ou telle exception peut "faire des bulles", alors vous, en tant que concepteur, avez échoué et, par conséquent, mon système n'est pas aussi stable qu'il pourrait l'être.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X