L'affaire contre les exceptions vérifiées

456

Depuis un certain nombre d'années, je n'ai pas pu 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, lu des choses sur des blogs, lu ce que Bruce Eckel avait à dire (la première personne que j'ai vue se prononcer contre eux).

J'écris actuellement du nouveau code et je fais très attention à la façon dont je traite les exceptions. J'essaie de voir le point de vue de la foule «nous n'aimons pas les exceptions vérifiées» et je ne le vois toujours pas.

Chaque conversation que j'ai se termine avec la même question sans réponse ... permettez-moi de la configurer:

En général (d'après la conception de Java),

  • Error est pour des choses qui ne devraient jamais être attrapées (VM a une allergie aux arachides et quelqu'un a laissé tomber un pot d'arachides dessus)
  • RuntimeException est pour des choses que le programmeur a mal faites (le programmeur est sorti de la fin d'un tableau)
  • Exception(sauf RuntimeException) concerne les éléments hors du contrôle du programmeur (le disque se remplit lors de l'écriture dans le système de fichiers, la limite de traitement des 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'exceptions.

Un argument commun 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 commun que j'entends est que les exceptions vérifiées rendent plus difficile la refactorisation du code.

Pour l'argument "tout ce que je vais faire, c'est quitter", je dis que même si vous quittez, vous devez afficher un message d'erreur raisonnable. Si vous tentez simplement de gérer les erreurs, vos utilisateurs ne seront pas trop satisfaits lorsque le programme se fermera sans une indication claire de pourquoi.

Pour la foule «il est difficile de refactoriser», 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 IOExceptiondevrait être transformé en une exception plus adaptée à ce qui se passe.

Je n'ai pas de problème avec le wrapping de Main avec catch(Exception)(ou dans certains cas catch(Throwable)pour m'assurer que le programme peut se terminer correctement - mais j'attrape toujours les exceptions spécifiques dont j'ai besoin. Cela me permet, à tout le moins, d'afficher une Message d'erreur.

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

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

Si la réponse est catch, Exceptionvous traitez également les erreurs de programmation de la même manière que les exceptions système. Cela me semble mal.

Si vous interceptez, Throwablevous traitez les exceptions système et les erreurs de machine virtuelle (et similaires) de la même manière. Cela me semble mal.

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

Je dirais qu'un programme qui affiche une trace de pile est faux. Les personnes qui n'aiment pas les exceptions vérifiées ne le pensent-elles pas?

Donc, si vous n'aimez pas les exceptions vérifiées, pouvez-vous expliquer pourquoi pas ET répondre à la question qui n'a pas de réponse s'il vous plaît?

Edit: Je ne cherche pas de conseils sur le moment d'utiliser l'un ou l'autre modèle, ce que je cherche, c'est pourquoi les gens s'étendent à partir de RuntimeExceptionparce qu'ils n'aiment pas s'étendre à partir de Exceptionet / ou pourquoi ils attrapent une exception, puis rejettent un RuntimeExceptionplutôt que d'ajouter des lancers à leur méthode. Je veux comprendre la motivation pour ne pas aimer les exceptions cochées.

TofuBeer
la source
46
Je ne pense pas que ce soit complètement subjectif - c'est une fonctionnalité de langage qui a été conçue pour avoir une utilisation spécifique, plutôt que pour que chacun décide de ce que c'est pour lui-même. Et ce n'est pas spécialement argumentatif, il aborde à l'avance des réfutations spécifiques que les gens auraient facilement pu trouver.
Gareth
6
Allons. Considéré comme une fonctionnalité linguistique, ce sujet a été et peut être abordé de manière objective.
Kurt Schelfthout
6
@cletus "répondre à votre propre question" si j'avais eu la réponse, je n'aurais pas posé la question!
TofuBeer
8
Grande question. En C ++, il n'y a aucune exception vérifiée, et à mon avis, cela rend la fonction d'exception inutilisable. Vous vous retrouvez dans une situation où vous devez mettre un crochet autour de chaque appel de fonction que vous faites, parce que vous ne savez tout simplement pas si cela pourrait lancer quelque chose.
Dimitri C.
4
L'argument le plus fort que je connaisse pour les exceptions vérifiées est qu'ils n'étaient pas à l'origine à Java et que lorsqu'ils ont été introduits, ils ont découvert des centaines de bogues dans le JDK. C'est un peu avant Java 1.0. Personnellement, je ne serais pas sans eux et je suis en désaccord violent avec Bruce Eckel et d'autres à ce sujet.
Marquis de Lorne

Réponses:

277

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

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

Fan que je sois de Hejlsberg et de son travail, cet argument m'a toujours semblé faux. Cela se résume essentiellement à:

"Les exceptions vérifiées sont mauvaises parce que les programmeurs les abusent simplement en les attrapant toujours et en les rejetant, ce qui conduit à des problèmes cachés et ignorés 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 simplement (par opposition à l'attraper avec un bloc catch vide) et l'utilisateur le verra.

Le résumé du résumé de l'argument est que "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 Goslings pour ne pas mettre des remplacements d'opérateurs en Java vient d'un argument similaire - ils confondent le programmeur car ils sont souvent abusés.

Mais à la fin, je trouve que c'est un faux argument de Hejlsberg et peut-être post-hoc créé pour expliquer le manque plutôt qu'une décision mûrement réfléchie.

Je dirais que, même si la surutilisation des exceptions vérifiées est une mauvaise chose et a tendance à entraîner une gestion bâclée par les utilisateurs, mais leur utilisation appropriée permet au programmeur API de donner un grand avantage au programmeur client API.

Maintenant, le programmeur API doit faire attention à ne pas lancer d'exceptions vérifiées partout, sinon ils ennuieront simplement le programmeur client. Le programmeur client très paresseux aura recours à la capture (Exception) {}comme Hejlsberg l'avertit et tous les avantages seront perdus et l'enfer s'ensuivra. Mais dans certaines circonstances, rien ne peut remplacer une bonne exception vérifiée.

Pour moi, l'exemple classique est l'API d'ouverture de fichier. Chaque langage de programmation de 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 cela: Chaque programmeur client utilisant cette API doit savoir qu'il doit gérer ce cas. Et il y a le hic: le programmeur d'API peut-il les aider à savoir qu'ils doivent y faire face en commentant seuls ou peut-il en effet insister le client s'en occupe.

En C, l'idiome va quelque chose comme

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

fopenindique l'échec en renvoyant 0 et C (bêtement) vous permet de traiter 0 comme un booléen et ... Fondamentalement, vous apprenez cet idiome et vous êtes d'accord. Mais si vous êtes un noob et que vous n'avez pas appris l'idiome. Ensuite, bien sûr, vous commencez avec

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

et apprenez à la dure.

Notez que nous ne parlons ici que de 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 large éventail de fonctionnalités (méthodes) à utiliser avec un protocole clairement défini pour chacune.

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

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

Le point que j'essaie de faire est le suivant: dans un langage de type statique, le programmeur d'API encourage le client à utiliser correctement l'API en empêchant son code client de se compiler s'il fait 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 se compilera. Pourquoi harceler 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 de type statique.)

Alors, qu'en est-il des exceptions vérifiées?

Eh bien, 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
}

Vous voyez cette prise? Voici la signature de cette méthode API:

public FileInputStream(String name)
                throws FileNotFoundException

Notez qu'il FileNotFoundExceptions'agit d'une exception vérifiée .

Le programmeur API vous dit ceci: "Vous pouvez utiliser ce constructeur pour créer un nouveau FileInputStream mais vous

a) doit transmettre le nom du fichier sous forme de chaîne
b) doit accepter la possibilité que le fichier ne soit pas trouvé lors de l'exécution "

Et c'est tout le point en ce qui me concerne.

La clé est fondamentalement ce que la question indique comme "des choses qui sont hors du contrôle du programmeur". Ma première pensée a été qu'il veut dire des choses qui ne sont pas dans l' API contrôle des programmeurs d' . Mais en fait, les exceptions vérifiées lorsqu'elles sont utilisées correctement devraient vraiment concerner des choses qui sont hors du contrôle du programmeur client et du programmeur API. Je pense que c'est la clé pour ne pas abuser des exceptions vérifiées.

Je pense que le fichier ouvert illustre bien le point. Le programmeur d'API sait que vous pourriez leur donner un nom de fichier qui s'avère inexistant au moment de l'appel de l'API, et qu'il ne pourra pas vous retourner ce que vous vouliez, mais devra lever une exception. Ils savent é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 il peut aussi être erroné au moment de l'exécution pour des raisons indépendantes de sa volonté.

Donc, l'API le rend explicite: il y aura des cas où ce fichier n'existe pas au moment où vous m'appelez et vous feriez bien mieux de le gérer.

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

public RowData getRowData(int row) 

Maintenant, en tant que programmeur d'API, je sais qu'il y aura des cas où certains clients transmettront une valeur négative pour la ligne ou une valeur de ligne en dehors de la table. Je pourrais donc être tenté de lever une exception vérifiée et d'obliger le client à y faire face:

public RowData getRowData(int row) throws CheckedInvalidRowNumberException

(Je ne l'appellerais pas vraiment "Checked" bien sûr.)

C'est une mauvaise utilisation des exceptions vérifiées. Le code client va être plein d'appels pour récupérer les données de ligne, chacun d'entre eux devra 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 entourant ma vue de table, elle ne devrait pas laisser l'utilisateur entrer dans un état où une ligne illégale est demandée. C'est donc un bug de la part du programmeur client.

Le programmeur API peut toujours prédire que le client va coder de tels bogues et devrait le gérer avec une exception d'exécution comme un IllegalArgumentException.

Avec une exception cochée getRowData, c'est clairement un cas qui va conduire le programmeur paresseux de Hejlsberg à simplement ajouter des captures vides. Lorsque cela se produit, les valeurs de ligne illégales ne seront pas évidentes même pour le testeur ou le débogage du développeur client, mais elles entraîneront plutôt des erreurs d'activation difficiles à localiser. Les roquettes Arianne exploseront après leur lancement.

D'accord, voici donc le problème: je dis que l'exception vérifiée FileNotFoundExceptionn'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 CheckedInvalidRowNumberExceptionc'est un gros inconvénient, conduisant à 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 ne suis pas content de jeter le bébé avec l'eau du bain ici, alors permettez-moi d'extraire quelques règles ici pour distinguer les bonnes exceptions vérifiées des mauvaises:

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

    Les exceptions cochées ne doivent être utilisées que lorsque le cas d'erreur est hors de contrôle à la fois de l'API et du programmeur client. Cela a à voir avec l' ouverture ou la fermeture du système. Dans une interface utilisateur contrainte où le programmeur client a le contrôle, par exemple, sur tous les boutons, commandes clavier, etc. qui ajoutent et suppriment des lignes de la vue de table (un système fermé), il s'agit d'un bogue de programmation client s'il tente d'extraire des données de une ligne inexistante. Dans un système d'exploitation basé sur des fichiers où un nombre illimité d'utilisateurs / applications peuvent ajouter et supprimer des fichiers (un système ouvert), il est concevable que le fichier que le client demande a été supprimé à leur insu, de sorte qu'ils devraient être censés y faire face. .

  2. Ubiquité:

    Les exceptions cochées ne doivent pas être utilisées sur un appel d'API effectué fréquemment par le client. Par fréquemment, je veux dire de beaucoup d'endroits dans le code client - pas souvent à temps. Donc, un code client n'a pas tendance à essayer d'ouvrir beaucoup le même fichier, mais ma vue de table se fait RowDatapartout à partir de différentes méthodes. En particulier, je vais écrire beaucoup de code comme

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

    et il sera douloureux de devoir en finir avec try / catch à chaque fois.

  3. Informer l'utilisateur:

    Les exceptions cochées doivent être utilisées dans les cas où vous pouvez imaginer qu'un message d'erreur utile soit présenté à l'utilisateur final. C'est le "et que ferez-vous quand cela se produira?" question que j'ai soulevée ci-dessus. Il se rapporte également au point 1. Puisque vous pouvez prévoir que quelque chose en dehors de votre système API client pourrait empêcher le fichier d'être là, 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é provoqué par un bogue interne et sans faute de l'utilisateur, il n'y a vraiment aucune information utile que vous pouvez leur donner. Si votre application ne laisse pas les exceptions d'exécution passer par la console, elle finira probablement par leur donner un message laid comme:

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

    En bref, si vous ne pensez pas que votre programmeur client peut expliquer votre exception d'une manière qui aide l'utilisateur, vous ne devriez probablement pas utiliser d'exception vérifiée.

Voilà donc mes règles. Un peu artificiel, et il y aura sans doute des exceptions (veuillez m'aider à les affiner si vous le souhaitez). Mais mon argument principal est qu'il existe des cas comme ceux FileNotFoundExceptionoù l'exception vérifiée est une partie du contrat API aussi importante et utile que les types de paramètres. Nous ne devons donc pas nous en passer simplement parce qu'il est mal utilisé.

Désolé, je ne voulais pas rendre cela si long et gaufré. 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 encapsulée (google it) dès le début de votre développement. JDK 1.4 et versions ultérieures fournissent un constructeur RuntimeExceptionpour cela, mais vous pouvez également créer facilement le vôtre. Voici le constructeur:

public RuntimeException(Throwable cause)

Ensuite, prenez l'habitude de gérer une exception vérifiée et vous vous sentez paresseux (ou vous pensez que le programmeur d'API était trop zélé en utilisant l'exception vérifiée en premier lieu), ne vous contentez pas d'avaler l'exception, enveloppez-la et le repousser.

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 le traiter après avoir vu le problème lors de l'exécution. Parce que, croyez-moi (et Anders Hejlsberg), vous ne reviendrez jamais à ce TODO dans votre

catch (Exception e) { /* TODO deal with this at some point (yeah right) */}
Rhubarbe
la source
110
L'ouverture de fichiers est en fait un bon exemple d'exceptions vérifiées totalement contre-productives. Parce que la plupart du temps, le code qui ouvre le fichier ne peut rien faire d'utile à l'exception - il vaut mieux faire plusieurs couches dans la pile des appels. L'exception vérifiée vous oblige à encombrer vos signatures de méthode juste pour que vous puissiez faire ce que vous auriez fait de toute façon - gérer l'exception à l'endroit où il est le plus logique de le faire.
Michael Borgwardt
116
@Michael: D'accord, vous pourriez généralement gérer ces exceptions d'E / S plusieurs niveaux plus haut - mais je ne vous entends pas nier qu'en tant que client API, vous devez les anticiper. Pour cette raison, je pense que les exceptions vérifiées sont appropriées. Oui, vous allez devoir déclarer des "lancers" sur chaque pile d'appels de méthode, mais je ne suis pas d'accord pour dire que c'est du fouillis. Il s'agit d'informations déclaratives utiles dans vos signatures de méthode. Vous dites: mes méthodes de bas niveau peuvent rencontrer un fichier manquant, mais elles vous en laisseront la gestion. Je ne vois rien à perdre et seulement du bon code propre à gagner
Rhubarbe
23
@Rhubarb: +1, réponse très intéressante, montre des arguments des deux côtés. Génial. À propos de votre dernier commentaire: notez que déclarer des levées sur chaque méthode dans la pile d'appels n'est pas toujours possible, surtout si vous implémentez une interface en cours de route.
R. Martinho Fernandes
8
C'est un argument très fort (et je suis d'accord avec lui en général), mais il y a un cas où cela ne fonctionne pas: les rappels génériques. Si j'ai une bibliothèque qui appelle un code défini par l'utilisateur, il n'y a aucun moyen (à ma connaissance, en java) pour que cette bibliothèque propage les exceptions vérifiées du code utilisateur qu'elle appelle au code utilisateur qui l'appelle. Ce problème s'étend également à d'autres parties du système de types de nombreux langages typés statiquement qui ne prennent pas en charge les types génériques appropriés. Cette réponse serait améliorée par une mention de cela (et peut-être une réponse).
Mankarse
7
Mauvais @Rhubarb. Les exceptions vérifiées étaient destinées aux "imprévus" qui pouvaient et devraient être traités, mais étaient toujours incompatibles avec les meilleures pratiques "lancer tôt, rattraper tard". L'ouverture d'un fichier est un exemple choisi avec soin, qui peut être géré. La plupart des IOException (par exemple l'écriture d'un octet), SQLException, RemoteException sont impossibles à gérer de manière significative. L'échec ou la nouvelle tentative doit être au niveau "métier" ou "demande", et les exceptions vérifiées - telles qu'utilisées dans les bibliothèques Java - sont une erreur qui rend cela difficile. literatejava.com/exceptions/…
Thomas W
184

La chose au sujet des exceptions vérifiées est qu'elles ne sont pas vraiment des exceptions par la compréhension habituelle 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'appels peut bouillonner et être gérée par du code quelque part plus haut, sans que le code intermédiaire n'ait à s'en soucier. Les exceptions vérifiées, en revanche, nécessitent que chaque niveau de code entre le lanceur et le receveur déclare qu'il connaît toutes les formes d'exceptions qui peuvent les traverser. C'est très peu différent dans la pratique si les exceptions vérifiées n'étaient que des valeurs de retour spéciales que l'appelant devait 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 que beaucoup de code, y compris une grande partie de la bibliothèque standard, utilise à mauvais escient les exceptions vérifiées pour des conditions exceptionnelles réelles que vous pourriez très bien vouloir rattraper plusieurs niveaux. Pourquoi IOException n'est-il pas une RuntimeException? Dans toutes les autres langues, je peux laisser une exception d'E / S 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 deux méthodes par rapport à l'exemple que vous souhaitez capturer toutes les IOExceptions de l'ensemble du processus d'écriture en flux, abandonner le processus et sauter dans le code de rapport d'erreurs; en Java, vous ne pouvez pas faire cela sans ajouter «jette IOException» à chaque niveau d'appel, même les niveaux qui eux-mêmes ne font pas d'E / S. De telles méthodes ne devraient pas avoir besoin de connaître la gestion des exceptions; avoir à ajouter des exceptions à leurs signatures:

  1. augmente inutilement le couplage;
  2. rend les signatures d'interface très fragiles à modifier;
  3. rend le code moins lisible;
  4. est si ennuyeux que la réaction courante du programmeur est de vaincre le système en faisant quelque chose d'horrible comme 'jette l'exception', 'catch (Exception e) {}', ou en enveloppant tout dans une RuntimeException (ce qui rend le débogage plus difficile).

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

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

Lorsque vous devez encombrer votre code avec des saletés ridicules comme celle-ci, il n'est pas étonnant que les exceptions vérifiées reçoivent un tas de haine, même si ce n'est vraiment qu'une mauvaise conception d'API.

Un autre mauvais effet particulier est sur l'inversion de contrôle, où le composant A fournit un rappel au composant générique B. Le composant A veut pouvoir laisser une exception renvoyer de son rappel à l'endroit où il a appelé le composant B, mais il ne peut pas car cela changerait l'interface de rappel qui est fixée par B. A ne peut le faire qu'en encapsulant la véritable exception dans une RuntimeException, qui est encore plus un passe-partout de gestion des exceptions à écrire.

Les exceptions vérifiées telles qu'implémentées dans Java et sa bibliothèque standard signifient passe-partout, passe-partout, passe-partout. Dans une langue déjà verbeuse, ce n'est pas une victoire.

bobince
la source
14
Dans votre exemple de code, il serait préférable de chaîner les exceptions afin que la cause d'origine puisse être trouvée lors de la lecture des journaux: throw CanNeverHappenException (e);
Esko Luontola
5
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 sinon 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.
Monsieur Smith
15
@Mister: Ce que je dis, c'est que les exceptions vérifiées mises en œuvre en Java se comportent, dans la pratique, plus comme des valeurs de retour comme en C que les `` exceptions '' traditionnelles que nous pourrions reconnaître du C ++ et d'autres langages pré-Java. Et que l'OMI cela mène en effet à la confusion et à une mauvaise conception.
bobince
9
Convenez que l'utilisation abusive des exceptions standard par les bibliothèques standard a certainement ajouté à la confusion et au mauvais comportement de capture. Et, souvent, c'est juste à partir d'une mauvaise documentation, par exemple une méthode de démontage comme déconnecter () qui lève IOException quand "une autre erreur d'E / S se produit". Eh bien, je me déconnectais! Suis-je une fuite d'une poignée ou d'une autre ressource? Dois-je réessayer? Sans savoir pourquoi cela s'est produit, je ne peux pas déduire l'action que je dois entreprendre et je dois donc deviner si je devrais simplement l'avaler, réessayer ou cautionner.
charstar
14
+1 pour "Valeurs de retour alternatives de l'API". Façon intéressante de voir les exceptions vérifiées.
Zsolt Török
75

Plutôt que de ressasser toutes les (nombreuses) raisons contre les exceptions vérifiées, je n'en choisirai qu'une. 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 effectuent tout nettoyage nécessaire (ou du moins ils le devraient).

J'ai également perdu le compte du nombre de fois où j'ai vu ceci:

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

Pourquoi? Parce que quelqu'un devait y faire face et était paresseux. C'était mal? Sûr. Cela arrive-t-il? Absolument. Et si c'était une exception non vérifiée à la place? L'application vient de mourir (ce qui est préférable à une exception).

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

Ok, je suppose que c'était trois raisons.

cletus
la source
4
Mais si une exception est correctement interceptée, vous pouvez informer l'utilisateur, effectuer d'autres tâches (journal?) 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.
Monsieur Smith
3
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 consiste pour IOExceptions à ajouter le nom de chemin absolu () du fichier en question.
Thorbjørn Ravn Andersen
15
Je pense que les IDE comme Eclipse ont beaucoup à blâmer pour le nombre de fois où vous avez vu le bloc catch vide. Vraiment, ils devraient recommencer par défaut.
artbristol
12
"99% du temps, je ne peux rien y faire" - faux, vous pouvez montrer à l'utilisateur un message disant "Impossible de se connecter au serveur" ou "Le périphérique IO a échoué", au lieu de simplement laisser l'application se bloquer en raison d'un petit hoquet de réseau. Vos deux exemples sont des œuvres d'art de mauvais programmeurs. Vous devriez attaquer les mauvais programmeurs et ne pas vérifier les exceptions elles-mêmes. C'est comme moi d'attaquer l'insuline pour ne pas aider avec mon diabète quand je l'utilise comme vinaigrette.
AxiomaticNexus
2
@YasmaniLlanes Vous ne pouvez pas toujours faire ces choses. Parfois, vous avez une interface à respecter. Et cela est particulièrement vrai lorsque vous concevez de bonnes API maintenables, car vous ne pouvez pas simplement commencer à lancer des effets secondaires partout. Cela et la complexité que cela introduira vous mordront gravement à grande échelle. Alors oui, 99% du temps, il n'y a rien à faire.
MasterMastic
50

Je sais que c'est une vieille question, mais j'ai passé un certain temps à lutter avec les exceptions vérifiées et j'ai quelque chose à ajouter. Veuillez me pardonner pour la longueur de celui-ci!

Mon bœuf principal, avec des exceptions vérifiées, est qu'il ruine le polymorphisme. Il est impossible de les faire jouer correctement avec des interfaces polymorphes.

Prenez la bonne Listinterface Java . Nous avons des implémentations en mémoire communes comme ArrayListet LinkedList. Nous avons également la classe skeletal AbstractListqui facilite la conception de nouveaux types de liste. Pour une liste en lecture seule, nous devons implémenter seulement deux méthodes: size()et get(int index).

Cet exemple de WidgetListclasse lit certains objets de taille fixe de type Widget(non représentés) dans 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' Listinterface familière , vous pouvez récupérer des éléments ( list.get(123)) ou itérer une liste ( for (Widget w : list) ...) sans avoir besoin de se connaître WidgetList. On peut transmettre cette liste à toutes les méthodes standard qui utilisent des listes génériques, ou l'encapsuler dans un Collections.synchronizedList. Le code qui l'utilise n'a besoin ni de savoir ni de se soucier si les "Widgets" sont constitués sur place, proviennent d'un tableau, ou sont lus à partir d'un fichier, ou d'une base de données, ou à travers le réseau, ou à partir d'un futur relais sous-espace. Cela fonctionnera toujours correctement car l' Listinterface est correctement implémentée.

Sauf que ce n'est pas le cas. La classe ci-dessus ne se compile pas car les méthodes d'accès aux fichiers peuvent lever une IOExceptionexception vérifiée que vous devez "attraper ou spécifier". Vous ne pouvez pas le spécifier comme jeté - le compilateur ne vous le permettra pas car cela violerait le contrat de l' Listinterface. Et il n'y a aucun moyen utile qui WidgetListpuisse gérer l'exception (comme je l'expliquerai plus tard).

Apparemment, la seule chose à faire est d'attraper et de renvoyer les exceptions vérifiées comme une exception non vérifiée:

@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é une UncheckedIOExceptionclasse pour exactement ce cas: pour attraper et repousser les IOExceptions à travers les frontières des méthodes polymorphes. Cela prouve mon point de vue!))

Les exceptions vérifiées ne fonctionnent tout simplement pas dans des cas comme celui-ci. Tu ne peux pas les jeter. Idem pour un intelligent Mapsoutenu par une base de données, ou une implémentation de java.util.Randomconnecté à 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 laisseront toujours pas en paix, car vous devez toujours intercepter et renvoyer l'une des méthodes de niveau inférieur, encombrant le code et encombrant la trace de la pile.

Je trouve que l'omniprésent Runnable interface est souvent sauvegardée dans ce coin, si elle appelle quelque chose qui lève des exceptions vérifiées. Il ne peut pas lever l'exception tel quel, donc tout ce qu'il peut faire est d'encombrer le code en attrapant et en relançant en tant que RuntimeException.

En fait, vous pouvez lever des exceptions cochées non déclarées si vous recourez à des hacks. La JVM, au moment de l'exécution, ne se soucie pas des règles d'exception vérifiées, nous devons donc tromper uniquement le compilateur. La façon la plus simple de procéder consiste à abuser des génériques. C'est ma méthode pour cela (nom de classe affiché car (avant Java 8) il est requis dans la syntaxe d'appel pour 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 cela, nous pouvons lancer une exception vérifiée à n'importe quelle profondeur de la pile sans la déclarer, sans l'envelopper dans un RuntimeException, et sans encombrer la trace de la pile! En utilisant à nouveau l'exemple "WidgetList":

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

Malheureusement, la dernière insulte des exceptions vérifiées est que le compilateur refuse de vous autoriser à intercepter une exception vérifiée si, à son avis erroné, elle n'aurait pas pu être levée. (Les exceptions non vérifiées n'ont pas cette règle.) Pour intercepter l'exception levée sournoisement, nous devons le faire:

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 gênant, mais du côté positif, il est toujours légèrement plus simple que le code pour extraire une exception vérifiée qui a été enveloppée dans un RuntimeException.

Heureusement, la throw t;déclaration est légale ici, même si le type de test vérifié, grâce à une règle ajoutée dans Java 7 sur la reprise des exceptions interceptées.


Lorsque les exceptions vérifiées rencontrent le polymorphisme, le cas contraire est également un problème: lorsqu'une méthode est spécifiée comme pouvant déclencher une exception vérifiée, mais pas une implémentation remplacée. Par exemple, la classe abstraite OutputStream« s writeméthodes spécifient tous throws IOException. ByteArrayOutputStreamest une sous-classe qui écrit dans un tableau en mémoire au lieu d'une véritable source d'E / S. Ses writeméthodes substituées ne peuvent pas provoquer de IOExceptions, elles n'ont donc pas de throwsclause, et vous pouvez les appeler sans vous soucier de l'exigence catch-or-specify.

Sauf pas toujours. Supposons qu'il Widgetdispose d'une méthode pour l'enregistrer dans un flux:

public void writeTo(OutputStream out) throws IOException;

Déclarer cette méthode pour accepter une plaine OutputStreamest la bonne chose à faire, elle peut donc être utilisée de manière polymorphe avec toutes sortes de sorties: fichiers, bases de données, réseau, etc. Et des tableaux en mémoire. Avec un tableau en mémoire, cependant, il existe une exigence fallacieuse 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, les exceptions vérifiées gênent. Si vos variables sont déclarées comme un type de base ayant des exigences d'exception plus ouvertes, vous devez ajouter des gestionnaires pour ces exceptions même si vous savez qu'elles ne se produiront pas dans votre application.

Mais attendez, les exceptions vérifiées sont si ennuyeuses qu'elles ne vous permettront même pas de faire l'inverse! Imaginez que vous interceptiez actuellement tout IOExceptionjeté par des writeappels sur un OutputStream, mais vous voulez changer le type déclaré de la variable en unByteArrayOutputStream , le compilateur vous réprimandera pour avoir tenté de capturer 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'une des trois writeméthodes de OutputStreamn'est pas remplacée par ByteArrayOutputStream. Plus précisément, write(byte[] data)est une méthode pratique 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. ByteArrayOutputStreamremplace la méthode à trois arguments mais hérite la méthode de commodité à un argument telle quelle. La méthode héritée fait exactement ce qu'il faut, mais elle inclut une throwsclause indésirable . C'était peut-être une erreur dans la conception de ByteArrayOutputStream, mais ils ne peuvent jamais y remédier car cela romprait la compatibilité des sources avec tout code qui intercepte l'exception - l'exception qui n'a jamais, n'est jamais et ne sera jamais levée!

Cette règle est également gênante lors de l'édition et du débogage. Par exemple, parfois je commenterai temporairement un appel de méthode, et s'il aurait pu lever une exception vérifiée, le compilateur se plaindra maintenant de l'existence du local tryet des catchblocs. Je dois donc les commenter également, et maintenant lors de la modification du code à l'intérieur, l'IDE se mettra en retrait au mauvais niveau car les {et }sont commentés. Gah! C'est une petite plainte, mais il semble que la seule chose que les exceptions vérifié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'appels , il n'y a rien d'utile à faire avec eux. Idéalement, en cas de problème, nous aurions un gestionnaire d'application spécifique compétent qui pourrait informer l'utilisateur du problème et / ou terminer ou réessayer l'opération selon le cas. Seul un gestionnaire en haut de la pile peut le faire car c'est le seul qui connaît l'objectif global.

Au lieu de cela, nous obtenons l'idiome suivant, qui est endémique comme moyen de fermer le compilateur:

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

Dans une interface graphique ou un programme automatisé, le message imprimé ne sera pas vu. Pire, il continue avec le reste du code après l'exception. L'exception n'est-elle pas réellement une erreur? Alors ne l'imprimez pas. Sinon, quelque chose d'autre va exploser dans un instant, date à laquelle l'objet d'exception d'origine aura disparu. Cet idiome n'est pas meilleur que BASIC On Error Resume Nextou PHP error_reporting(0);.

Appeler une sorte de classe logger n'est pas beaucoup mieux:

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

C'est tout aussi paresseux que e.printStackTrace();et continue de travailler avec du 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 est plus haut dans la pile des appels (ou il est défini comme gestionnaire d'exceptions non intercepté du thread ). Donc, dans la plupart des endroits, tout ce que vous avez à faire est de lever l'exception plus haut dans la pile . Par exemple, throw e;. Les exceptions vérifiées ne font qu'empêcher.

Je suis sûr que les exceptions vérifiées semblaient être une bonne idée lorsque le langage a été conçu, mais dans la pratique, je les ai trouvées gênantes et sans aucun avantage.

Boann
la source
Pour votre méthode de taille avec WidgetList, je mettrais en cache la taille dans une variable et la définirais dans le constructeur. Le constructeur est libre de lever une exception. Cela ne fonctionnera pas si le fichier change lors de l'utilisation de WidgetList, ce qui serait probablement mauvais si c'était le cas.
TofuBeer
2
SomeStupidExceptionOmgWhoCares bien que quelqu'un se souciait assez de le jeter. Donc, soit il n'aurait jamais dû être jeté (mauvaise conception), soit vous devriez vraiment le manipuler. Il en va de même de la mauvaise implémentation d'une classe pré-1.0 (le flux de sortie du tableau d'octets) où la conception était, malheureusement mauvaise.
TofuBeer
2
L'idiome approprié aurait été une directive qui intercepterait toutes les exceptions spécifiées levées par des appels de sous-programmes imbriqués et les renverrait enveloppés dans un RuntimeException. Notez qu'une routine peut être déclarée simultanément throws IOExceptionet spécifier également que tout IOExceptionappel provenant d'un appel imbriqué doit être considéré comme inattendu et encapsulé.
supercat
15
Je suis un développeur professionnel C # avec une certaine expérience Java qui est tombé sur ce post. Je suis déconcerté de savoir pourquoi quelqu'un soutiendrait ce comportement bizarre. Dans .NET, si je veux attraper un type d'exception spécifique, je peux l'attraper. Si je veux juste le laisser tomber dans la pile, il n'y a rien à faire. J'aimerais que Java ne soit pas si excentrique. :)
aikeru
Concernant "parfois, je commenterai temporairement un appel de méthode" - j'ai appris à l'utiliser if (false)pour cela. Cela évite le problème de la clause throw et l'avertissement m'aide à revenir en arrière plus rapidement. +++ Cela dit, je suis d'accord avec tout ce que vous avez écrit. Les exceptions vérifiées ont une certaine valeur, mais cette valeur est négligeable par rapport à leur coût. Presque toujours, ils gênent.
maaartinus
45

Eh bien, il ne s'agit pas d'afficher une trace de pile ou de planter silencieusement. Il s'agit de pouvoir communiquer des erreurs entre les couches.

Le problème avec les 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, vous devez continuer à ajouter des déclarations de lancers sur l'ensemble de votre application. Cela signifie 1) qu'un nouveau type d'exception affectera de nombreuses signatures de fonction, et 2) que vous pouvez manquer une instance spécifique de l'exception que vous souhaitez réellement (par exemple, ouvrir 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 à cause de la signature throws IOException, il est facile de l'ignorer).

Je fais face à cette situation maintenant dans une application. Nous avons reconditionné presque des exceptions comme AppSpecificException. Cela a rendu les signatures vraiment propres et nous n'avons pas eu à nous soucier d'exploser throwsdans les signatures.

Bien sûr, nous devons maintenant spécialiser la gestion des erreurs aux niveaux supérieurs, en implémentant une logique de nouvelle tentative, etc. Tout est AppSpecificException, cependant, nous ne pouvons donc pas dire "Si une IOException est levée, réessayez" ou "Si ClassNotFound est levé, abandonnez complètement". Nous ne disposons pas d'un moyen fiable pour atteindre la vraie exception, car les choses sont reconditionnées encore et encore lorsqu'elles passent entre notre code et le code 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 bouillonne comme si vous l'aviez revu 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. Saisissez et gérez une exception spécifique . Il s'agit par exemple d'implémenter une logique de nouvelle tentative.
  2. Capturez et relancez d' autres 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". Ce sont des erreurs sur lesquelles vous ne pouvez rien faire; seul un niveau supérieur en sait assez pour le gérer.
  3. Attrapez tout et affichez un message d'erreur. Ceci est généralement à la racine même d'un répartiteur, et tout ce qu'il fait, il s'assure qu'il peut communiquer l'erreur à l'appelant via un mécanisme sans exception (dialogue contextuel, marshaling d'un objet d'erreur RPC, etc.).
Richard Levasseur
la source
5
Vous auriez pu créer des sous-classes spécifiques d'AppSpecificException pour permettre la séparation tout en conservant les signatures de méthode simples.
Thorbjørn Ravn Andersen
1
Un ajout très important à l'élément 2 est qu'il vous permet d'ajouter des informations à l'exception interceptée (par exemple, en imbriquant une RuntimeException). Il est beaucoup, beaucoup mieux d'avoir le nom du fichier introuvable dans la trace de la pile, que caché au fond d'un fichier journal.
Thorbjørn Ravn Andersen
1
Fondamentalement, votre argument est "La gestion des exceptions est fatigante, donc je préfère ne pas y faire face". À mesure que l'exception bouillonne, elle perd son sens et la création de contexte est pratiquement inutile. En tant que concepteur d'une API, vous devez établir clairement ce qui peut être attendu en cas de problème, si mon programme se bloque parce que je n'ai pas été informé que telle ou telle exception peut "bouillonner", vous, en tant que concepteur, avez échoué et comme suite à votre échec, mon système n'est pas aussi stable qu'il peut l'être.
Newtopian
4
Ce n'est pas du tout ce que je dis. Votre dernière phrase est en fait d'accord avec moi. Si tout est enveloppé dans AppSpecificException, alors il ne bouillonne pas (et le sens / contexte est perdu), et, oui, le client API n'est pas informé - c'est exactement ce qui se passe avec les exceptions vérifiées (comme elles le sont en java) , car les gens ne veulent pas traiter les fonctions avec beaucoup de throwsdéclarations.
Richard Levasseur
6
@Newtopian - les exceptions ne peuvent en grande partie être gérées qu'au niveau "métier" ou "demande". Il est logique d'échouer ou de réessayer à grande granularité, pas pour chaque petite défaillance potentielle. Pour cette raison, les meilleures pratiques de gestion des exceptions sont résumées comme «lancer tôt, attraper tard». Les exceptions vérifiées rendent plus difficile la gestion de la fiabilité au niveau correct et encouragent un grand nombre de blocs de capture mal codés. literatejava.com/exceptions/…
Thomas W
24

SNR

Premièrement, les exceptions vérifiées diminuent le "rapport signal / bruit" pour le code. Anders Hejlsberg parle également de la programmation impérative vs déclarative qui est un concept similaire. Dans tous les cas, tenez compte des extraits de code suivants:

Mettre à jour l'interface utilisateur à partir d'un thread non UI en Java:

try {  
    // Run the update code on the Swing thread  
    SwingUtilities.invokeAndWait(() -> {  
        try {
            // Update UI value from the file system data  
            FileUtility f = new FileUtility();  
            uiComponent.setValue(f.readSomething());
        } catch (IOException e) {  
            throw new UncheckedIOException(e);
        }
    });
} catch (InterruptedException ex) {  
    throw new IllegalStateException("Interrupted updating UI", ex);  
} catch (InvocationTargetException ex) {
    throw new IllegalStateException("Invocation target exception updating UI", ex);
}

Mettre à jour l'interface utilisateur à partir d'un thread non UI en C #:

private void UpdateValue()  
{  
   // Ensure the update happens on the UI thread  
   if (InvokeRequired)  
   {  
       Invoke(new MethodInvoker(UpdateValue));  
   }  
   else  
   {  
       // Update UI value from the file system data  
       FileUtility f = new FileUtility();  
       uiComponent.Value = f.ReadSomething();  
   }  
}  

Ce qui me semble beaucoup plus clair. Lorsque vous commencez à faire de plus en plus d'interface utilisateur dans Swing, les exceptions vérifiées commencent à devenir vraiment ennuyeuses et inutiles.

Pause de prison

Pour implémenter même les implémentations les plus élémentaires, telles que l'interface de liste de Java, les exceptions vérifiées en tant qu'outil de conception par contrat tombent en panne. Considérez une liste qui est soutenue par une base de données ou un système de fichiers ou toute autre implémentation qui lève une exception vérifiée. La seule implémentation possible consiste à intercepter l'exception vérifiée et à la renvoyer en tant qu'exception non vérifiée:

@Override
public void clear()  
{  
   try  
   {  
       backingImplementation.clear();  
   }  
   catch (CheckedBackingImplException ex)  
   {  
       throw new IllegalStateException("Error clearing underlying list.", ex);  
   }  
}  

Et maintenant, vous devez vous demander quel est l'intérêt de tout ce code? Les exceptions vérifiées ajoutent simplement du bruit, l'exception a été interceptée mais pas gérée et la conception par contrat (en termes d'exceptions vérifiées) est tombée en panne.

Conclusion

  • La capture des exceptions est différente de leur traitement.
  • Les exceptions cochées ajoutent du bruit au code.
  • La gestion des exceptions fonctionne bien en C # sans eux.

Je blogué sur ce auparavant .

Luke Quinane
la source
3
Dans l'exemple, Java et C # ne font que propager les exceptions sans les gérer (Java via IllegalStateException). La différence est que vous souhaiterez peut-être gérer une FileNotFoundException mais il est peu probable que la gestion d'InvocationTargetException ou d'InterruptedException soit utile.
Luke Quinane
3
Et de la manière C #, comment puis-je savoir que l'exception d'E / S peut se produire? De plus, je ne lèverais jamais une exception de la course ... Je considère que l'abus de la gestion des exceptions. Désolé, mais pour cette partie de votre code, je peux encore voir votre côté.
TofuBeer
7
Nous y arrivons :-) Donc, à chaque nouvelle version d'une API, vous devez passer au peigne fin tous vos appels et rechercher de nouvelles exceptions qui pourraient se produire? Cela peut facilement se produire avec des API internes à une entreprise, car elles n'ont pas à se soucier de la compatibilité descendante.
TofuBeer
3
Voulez-vous dire réduire le rapport signal / bruit?
neo2862
3
@TofuBeer N'est-il pas obligé de mettre à jour votre code après que l'interface d'une API sous-jacente a changé une bonne chose? Si vous n'aviez eu que des exceptions non vérifiées, vous vous seriez retrouvé avec un programme cassé / incomplet sans le savoir.
Francois Bourgeois
23

Artima a publié une interview avec l'un des architectes de .NET, Anders Hejlsberg, qui couvre avec acuité les arguments contre les exceptions vérifiées. Un petit avant-goût:

La clause throws, au moins la façon dont elle est implémentée en Java, ne vous oblige pas nécessairement à gérer les exceptions, mais si vous ne les gérez pas, elle vous oblige à reconnaître précisément les exceptions qui pourraient passer. Cela vous oblige à intercepter les exceptions déclarées ou à les placer dans votre propre clause throws. Pour contourner cette exigence, les gens font des choses ridicules. Par exemple, ils décorent chaque méthode avec «jette l'exception». Cela défait complètement la fonctionnalité et vous venez de faire écrire au programmeur plus de gobbledy gunk. Cela n'aide personne.

Le Dude
la source
2
En fait, l'architecte en chef.
Piège
18
J'ai lu que, pour moi, son argument se résume à "il y a de mauvais programmeurs là-bas".
TofuBeer
6
TofuBeer, pas du tout. Le fait est que plusieurs fois vous ne savez pas quoi faire avec l'exception que la méthode appelée lève, et le cas qui vous intéresse vraiment n'est même pas mentionné. Vous ouvrez un fichier, vous obtenez une exception d'E / S, par exemple ... ce n'est pas mon problème, alors je le lance. Mais la méthode d'appel de niveau supérieur voudra simplement arrêter le traitement et informer l'utilisateur qu'il y a un problème inconnu. L'exception vérifiée n'a pas aidé du tout. C'était l'une des mille choses étranges qui peuvent arriver.
Dan Rosenstark
4
@yar, si vous n'aimez pas l'exception cochée, faites une "nouvelle exception RuntimeException (" nous ne nous y attendions pas en faisant Foo.bar () ", e)" et en finir avec.
Thorbjørn Ravn Andersen
4
@ ThorbjørnRavnAndersen: Une faiblesse fondamentale de la conception de Java, que .net a malheureusement copié, est qu'elle utilise le type d'une exception comme moyen principal pour décider si elle doit être appliquée et comme moyen principal d'indiquer le type général de chose. qui a mal tourné, alors qu'en fait les deux questions sont largement orthogonales. Ce qui importe n'est pas ce qui a mal tourné, mais les objets d'état. De plus, .net et Java supposent par défaut qu'agir et résoudre une exception sont généralement la même chose, alors qu'en fait ils sont souvent différents.
supercat
20

J'ai d'abord été d'accord avec vous, car j'ai toujours été en faveur des exceptions vérifiées, et j'ai commencé à réfléchir à la raison pour laquelle je n'aime pas ne pas avoir vérifié les exceptions dans .Net. Mais je me suis alors rendu compte que je ne fais pas comme des exceptions vérifiées.

Pour vous répondre, oui, j'aime que mes programmes montrent des traces de pile, de préférence vraiment laides. Je veux que l'application explose en un horrible tas de messages d'erreur les plus laids que vous puissiez souhaiter voir.

Et la raison en est que, si cela fait cela, je dois le réparer, et je dois le réparer tout de suite. Je veux savoir immédiatement qu'il y a un problème.

Combien de fois gérez-vous réellement les exceptions? Je ne parle pas d'attraper des exceptions - je parle de les gérer? Il est trop facile d'écrire ce qui suit:

try {
  thirdPartyMethod();
} catch(TPException e) {
  // this should never happen
}

Et je sais que vous pouvez dire que c'est une mauvaise pratique, et que «la réponse» est de faire quelque chose avec l'exception (laissez-moi deviner, enregistrez-vous?), Mais dans le monde réel (tm), la plupart des programmeurs ne font tout simplement pas il.

Alors oui, je ne veux pas faire d'exception si je ne suis pas obligé de le faire, et je veux que mon programme explose de façon spectaculaire quand je foirerai. Échouer en silence est le pire résultat possible.

tsimon
la source
Java vous encourage à faire ce genre de chose, afin que vous n'ayez pas à ajouter tous les types d'exceptions à chaque signature de méthode.
yfeldblum
17
Drôle .. depuis que j'ai embrassé correctement les exceptions vérifiées et les ai utilisées de manière appropriée, mes programmes ont cessé de exploser dans une énorme pile fumante d'insatisfaction client. Si pendant le développement, vous avez une grosse trace de pile mauvaise, le client est tenu de les obtenir également. D'love pour voir son visage quand il voit ArrayIndexOutOfBoundsException avec une trace de pile haute d'un mile sur son système en panne au lieu d'une petite notification de plateau disant que la configuration de couleur pour le bouton XYZ n'a pas pu être analysée, la valeur par défaut a donc été utilisée avec le logiciel qui bourdonnait joyeusement along
Newtopian
1
Peut-être que Java a besoin d'une déclaration "cantHandle" qui spécifierait qu'une méthode ou un bloc de code try / catch n'est pas préparé pour gérer une exception particulière qui se produit en son sein, et que toute exception de ce type qui se produit par un moyen autre qu'un explicite throw dans cette méthode (par opposition à une méthode appelée) doit être automatiquement encapsulé et renvoyé dans une RuntimeException. À mon humble avis, les exceptions vérifiées devraient rarement propager la pile d'appels sans être encapsulées.
supercat
3
@Newtopian - J'écris des serveurs et des logiciels haute fiabilité et je le fais depuis 25 ans. Mes programmes n'ont jamais explosé et je travaille avec des systèmes financiers et militaires à haute disponibilité, réessayez et reconnectons, basés sur l'intégration. J'ai une base objective absolue pour préférer les exceptions d'exécution. Les exceptions vérifiées compliquent la mise en œuvre des bonnes pratiques "lancer tôt, attraper tard". La fiabilité et la gestion des erreurs sont au niveau "métier", "connexion" ou "demande". (Ou occasionnellement lors de l'analyse des données). Les exceptions vérifiées empêchent de bien faire les choses.
Thomas W
1
Les exceptions dont vous parlez ici sont en RuntimeExceptionseffet que vous n'avez pas à attraper, et je conviens que vous devriez laisser le programme exploser. Les exceptions que vous devez toujours intercepter et gérer sont les exceptions vérifiées comme IOException. Si vous obtenez un IOException, il n'y a rien à corriger dans votre code; votre programme ne devrait pas exploser simplement parce qu'il y a eu un hoquet réseau.
AxiomaticNexus
20

L'article Exceptions Java efficaces explique bien quand utiliser les exceptions non cochées et quand utiliser les exceptions cochées. Voici quelques citations de cet article pour souligner les principaux points:

Imprévus: condition attendue exigeant une réponse alternative d'une méthode qui peut être exprimée en fonction de l'objectif de la méthode. L'appelant de la méthode attend ce genre de conditions et a une stratégie pour y faire face.

Erreur: condition imprévue qui empêche une méthode d'atteindre son objectif prévu qui ne peut être décrite sans référence à l'implémentation interne de la méthode.

(SO n'autorise pas les tableaux, vous voudrez peut-être lire ce qui suit à partir de la page d'origine ...)

Contingence

  • Est considéré comme: une partie de la conception
  • Devrait se produire: régulièrement mais rarement
  • Qui s'en soucie: le code en amont qui invoque la méthode
  • Exemples: modes de retour alternatifs
  • Meilleure cartographie: une exception vérifiée

Faute

  • Est considéré comme: une méchante surprise
  • Devrait se produire: jamais
  • Qui s'en soucie: les personnes qui doivent résoudre le problème
  • Exemples: bogues de programmation, dysfonctionnements matériels, erreurs de configuration, fichiers manquants, serveurs indisponibles
  • Meilleure cartographie: une exception non contrôlée
Esko Luontola
la source
Je sais quand les utiliser, je veux savoir pourquoi les gens qui ne suivent pas ce conseil ... ne suivent pas ce conseil :-)
TofuBeer
Que sont les bogues de programmation et comment les distinguer des bogues d'utilisation ? Est-ce un bug de programmation si l'utilisateur passe les mauvais arguments au programme? Du point de vue Java, ce n'est peut-être pas un bug de programmation, mais du point de vue du script shell, c'est un bug de programmation. Quels sont donc les arguments invalides args[]? S'agit-il d'une éventualité ou d'une faute?
ceving
3
@TofuBeer - Parce que les concepteurs de bibliothèques Java ont choisi de mettre toutes sortes de défaillances irrécupérables de bas niveau en tant qu'exceptions vérifiées alors qu'elles auraient clairement dû être décochées . FileNotFound est la seule exception IOException qui doit être vérifiée, par exemple. En ce qui concerne JDBC - se connecter uniquement à la base de données, peut raisonnablement être considéré comme une éventualité . Toutes les autres exceptions SQLE auraient dû être des échecs et non vérifiées . La gestion des erreurs doit se faire correctement au niveau «métier» ou «demande» - voir la meilleure pratique «lancer tôt, attraper tard». Les exceptions vérifiées sont un obstacle à cela.
Thomas W
1
Il y a un seul gros défaut dans votre argument. La "contingence" ne doit PAS être gérée via des exceptions, mais via des valeurs de retour de code et de méthode d'entreprise. Les exceptions sont pour, comme le mot le dit, des situations EXCEPTIONNELLES, donc des fautes.
Matteo Mosca
@MatteoMosca Les codes de retour d'erreur ont tendance à être ignorés et cela suffit pour les disqualifier. En fait, tout ce qui est inhabituel ne peut souvent être géré que quelque part dans la pile et c'est un cas d'utilisation pour les exceptions. Je pourrais imaginer quelque chose comme le File#openInputStreamretour Either<InputStream, Problem>- si c'est ce que vous voulez dire, alors nous pouvons être d'accord.
maaartinus
19

En bref:

Les exceptions sont une question de conception d'API. -- Ni plus ni moins.

L'argument des exceptions vérifiées:

Pour comprendre pourquoi les exceptions vérifiées peuvent ne pas être une bonne chose, tournons la question et demandons: Quand ou pourquoi les exceptions vérifiées sont-elles attrayantes, c'est-à-dire pourquoi voudriez-vous que le compilateur applique la déclaration d'exceptions?

La réponse est évidente: Parfois, vous devez intercepter une exception, et cela n'est possible que si le code appelé offre une classe d'exception spécifique pour l'erreur qui vous intéresse.

Par conséquent, l'argument pour les exceptions vérifiées est que le compilateur oblige les programmeurs à déclarer les exceptions levées et, espérons- le , le programmeur documentera également les classes d'exceptions spécifiques et les erreurs qui les provoquent.

En réalité cependant, trop souvent, un package com.acmene lance AcmeExceptionqu'une sous-classe plutôt que spécifique. Les appelants doivent alors gérer, déclarer ou re-signaler AcmeExceptions, mais ne peuvent toujours pas être sûrs si AcmeFileNotFoundErrorunAcmePermissionDeniedError .

Donc, si vous n'êtes intéressé que par un AcmeFileNotFoundError, la solution consiste à déposer une demande de fonctionnalité auprès des programmeurs ACME et à leur dire d'implémenter, déclarer et documenter cette sous-classe de AcmeException.

Alors pourquoi s'embêter?

Par conséquent, même avec des exceptions vérifiées, le compilateur ne peut pas forcer les programmeurs à lever des exceptions utiles . Ce n'est encore qu'une question de qualité de l'API.

En conséquence, les langues sans exceptions vérifiées ne se comportent généralement pas bien pire. Les programmeurs pourraient être tentés de lancer des instances non spécifiques d'une Errorclasse générale plutôt que d'un AcmeException, mais s'ils se soucient du tout de la qualité de leur API, ils apprendront à introduire unAcmeFileNotFoundError après tout.

Dans l'ensemble, la spécification et la documentation des exceptions ne diffèrent pas beaucoup de la spécification et de la documentation, disons, des méthodes ordinaires. Celles-ci sont également une question de conception de l'API, et si un programmeur a oublié d'implémenter ou d'exporter une fonctionnalité utile, l'API doit être améliorée afin que vous puissiez l'utiliser avec efficacité.

Si vous suivez cette ligne de raisonnement, il devrait être évident que le "tracas" de déclarer, intercepter et relancer des exceptions qui est si courant dans des langages comme Java ajoute souvent peu de valeur.

Il convient également de noter que la machine virtuelle Java n'a pas d'exceptions vérifiées - seul le compilateur Java les vérifie et les fichiers de classe avec les déclarations d'exceptions modifiées sont compatibles au moment de l'exécution. La sécurité de la machine virtuelle Java n'est pas améliorée par les exceptions vérifiées, uniquement par le style de codage.

David Lichteblau
la source
4
Votre argument s'oppose à lui-même. Si «parfois, vous devez intercepter une exception» et que la qualité de l'API est souvent médiocre, sans exceptions vérifiées, vous ne saurez pas si le concepteur a négligé de documenter qu'une certaine méthode lève une exception qui doit être interceptée. Ajoutez à cela le lancer AcmeExceptionplutôt que la AcmeFileNotFoundErrorchance de découvrir ce que vous avez fait de mal et où vous devez l'attraper. Les exceptions vérifiées offrent aux programmeurs un minimum de protection contre une mauvaise conception d'API.
Eva
1
La conception de la bibliothèque Java a commis de graves erreurs. Les `` exceptions vérifiées '' concernaient les imprévus prévisibles et récupérables - tels que fichier introuvable, échec de connexion. Ils n'ont jamais été conçus ou adaptés à une défaillance systémique de bas niveau. Il aurait été bien de forcer l'ouverture d'un fichier à vérifier, mais il n'y a pas de nouvelle tentative ou récupération raisonnable pour l'échec à écrire un seul octet / exécuter une requête SQL, etc. "niveau, qui vérifiait les exceptions rendait inutilement difficile. literatejava.com/exceptions/…
Thomas W
17

J'ai travaillé avec plusieurs développeurs au cours des trois dernières années dans des applications relativement complexes. Nous avons une base de code qui utilise assez souvent les exceptions vérifiées avec une gestion correcte des erreurs, et d'autres qui ne le font pas.

Jusqu'à présent, j'ai trouvé plus facile de travailler avec la base de code avec les exceptions vérifiées. Lorsque j'utilise l'API de quelqu'un d'autre, il est agréable de voir exactement à quel type de conditions d'erreur je peux m'attendre lorsque j'appelle le code et les traite correctement, soit en enregistrant, en affichant ou en ignorant (Oui, il existe des cas valides pour ignorer exceptions, comme une implémentation de ClassLoader). Cela donne au code que j'écris l'occasion de récupérer. Toutes les exceptions d'exécution que je propage jusqu'à ce qu'elles soient mises en cache et gérées avec un code générique de gestion des erreurs. Lorsque je trouve une exception vérifiée que je ne veux pas vraiment gérer à un niveau spécifique, ou que je considère comme une erreur de logique de programmation, je l'enveloppe dans une RuntimeException et la laisse bouillonner. Jamais, jamais avaler une exception sans une bonne raison (et les bonnes raisons de le faire sont plutôt rares)

Lorsque je travaille avec la base de code qui n'a pas d'exceptions vérifiées, cela me rend un peu plus difficile de savoir à l'avance à quoi puis-je m'attendre lors de l'appel de la fonction, ce qui peut briser terriblement certaines choses.

C'est bien sûr une question de préférence et de compétence de développeur. Les deux méthodes de programmation et de gestion des erreurs peuvent être tout aussi efficaces (ou inefficaces), donc je ne dirais pas qu'il existe The One Way.

Dans l'ensemble, je trouve plus facile de travailler avec Checked Exceptions, en particulier dans les grands projets avec beaucoup de développeurs.

Mario Ortegón
la source
6
Je le fais. Pour moi, ils sont une partie essentielle d'un contrat. Sans avoir à entrer dans les détails de la documentation de l'API, je peux rapidement connaître le plus probable des scénarios d'erreur.
Wayne Hartman
2
Se mettre d'accord. J'ai ressenti la nécessité de vérifier les exceptions dans .Net une fois lorsque j'ai essayé de passer des appels réseau. Sachant qu'un hoquet réseau pouvait survenir à tout moment, j'ai dû lire toute la documentation des API pour savoir quelle exception j'avais besoin d'attraper spécifiquement pour ce scénario. Si C # avait vérifié les exceptions, je l'aurais su immédiatement. D'autres développeurs C # laisseraient probablement l'application se bloquer à partir d'une simple erreur réseau.
AxiomaticNexus
13

Catégories d'exceptions

Lorsque je parle d'exceptions, je me réfère toujours à l' article du blog sur les exceptions d'Eex Lippert . Il place des exceptions dans ces catégories:

  • Fatal - Ces exceptions ne sont pas de votre faute : vous ne pouvez pas empêcher alors, et vous ne pouvez pas les gérer de manière sensible. Par exemple, OutOfMemoryErrorou ThreadAbortException.
  • Boneheaded - Ces exceptions sont de votre faute : vous auriez dû les empêcher, et elles représentent des bogues dans votre code. Par exemple, ArrayIndexOutOfBoundsException, NullPointerExceptionou tout IllegalArgumentException.
  • Vexing - Ces exceptions ne sont pas exceptionnelles , ce n'est pas votre faute, vous ne pouvez pas les empêcher, mais vous devrez y faire face. Ils sont souvent le résultat d'une décision de conception malheureuse, telle que le lancement NumberFormatExceptionau Integer.parseIntlieu de fournir une Integer.tryParseIntméthode qui renvoie un faux booléen en cas d'échec de l'analyse.
  • Exogène - Ces exceptions sont généralement exceptionnelles , ce n'est pas votre faute, vous ne pouvez pas (raisonnablement) les empêcher, mais vous devez les gérer . Par exemple FileNotFoundException,.

Un utilisateur d'API:

  • ne doit pas gérer les exceptions fatales ou osées
  • devrait gérer les exceptions de vexation , mais elles ne devraient pas se produire dans une API idéale.
  • doit gérer les exceptions exogènes .

Exceptions vérifiées

Le fait que l'utilisateur de l'API doive gérer une exception particulière fait partie du contrat de la méthode entre l'appelant et l'appelé. Le contrat spécifie, entre autres: le nombre et les types d'arguments attendus par l'appelé, le type de valeur de retour auquel l'appelant peut s'attendre et les exceptions que l'appelant doit gérer .

Étant donné que les exceptions de vexation ne doivent pas exister dans une API, seules ces exceptions exogènes doivent être vérifiées pour faire partie du contrat de la méthode. Relativement peu d'exceptions sont exogènes , donc toute API devrait avoir relativement peu d'exceptions vérifiées.

Une exception vérifiée est une exception qui doit être gérée . Gérer une exception peut être aussi simple que de l'avaler. Là! L'exception est gérée. Période. Si le développeur veut le gérer de cette façon, très bien. Mais il ne peut ignorer l'exception et a été prévenu.

Problèmes d'API

Mais toute API qui a vérifié les dérangements et les exceptions fatales (par exemple le JCL) mettra une pression inutile sur les utilisateurs de l'API. De telles exceptions doivent être gérées, mais soit l'exception est si courante qu'elle n'aurait pas dû être une exception en premier lieu, soit rien ne peut être fait lors de sa gestion. Et cela amène les développeurs Java à détester les exceptions vérifiées.

De plus, de nombreuses API n'ont pas de hiérarchie de classe d'exception appropriée, ce qui fait que toutes sortes de causes d'exception non exogènes sont représentées par une seule classe d'exception vérifiée (par exemple IOException). Et cela amène également les développeurs Java à détester les exceptions vérifiées.

Conclusion

Les exceptions exogènes sont celles qui ne sont pas de votre faute, qui n'auraient pas pu être évitées et qui devraient être traitées. Ceux-ci forment un petit sous-ensemble de toutes les exceptions qui peuvent être levées. Les API ne doivent avoir vérifié que les exceptions exogènes et toutes les autres exceptions décochées. Cela améliorera les API, mettra moins de pression sur l'utilisateur de l'API et réduira donc la nécessité de tout intercepter, d'avaler ou de rejeter des exceptions non contrôlées.

Ne détestez donc pas Java et ses exceptions vérifiées. Au lieu de cela, détestez les API qui surutilisent les exceptions vérifiées.

Daniel AA Pelsmaeker
la source
Et abusez-en en n'ayant pas de hiérarchie.
TofuBeer
1
FileNotFound et l'établissement d'une connexion JDBC / réseau sont des imprévus et des exceptions correctes à vérifier, car elles sont prévisibles et (éventuellement) récupérables. La plupart des autres IOExceptions, SQLExceptions, RemoteException etc. sont des échecs imprévisibles et irrécupérables , et auraient dû être des exceptions d'exécution. En raison d'une conception erronée de la bibliothèque Java, nous avons tous été confrontés à cette erreur et utilisons maintenant principalement Spring & Hibernate (qui a bien conçu sa conception).
Thomas W
Vous devez généralement gérer les exceptions à tête d'os, bien que vous ne souhaitiez peut-être pas l'appeler "gestion". Par exemple, dans un serveur Web, je les enregistre et en montre 500 à l'utilisateur. Comme l'exception est inattendue, il y a tout ce que je peux faire avant la correction du bogue.
maaartinus
6

D'accord ... Les exceptions vérifiées ne sont pas idéales et ont une mise en garde, mais elles servent à quelque chose. Lors de la création d'une API, il existe des cas spécifiques de défaillances contractuelles de cette API. Lorsque, dans le contexte d'un langage fortement typé statiquement tel que Java, si l'on n'utilise pas d'exceptions vérifiées, on doit s'appuyer sur une documentation et une convention ad hoc pour évoquer la possibilité d'erreur. Cela supprime tous les avantages que le compilateur peut apporter en cas d'erreur de gestion et vous êtes entièrement laissé à la bonne volonté des programmeurs.

Donc, on supprime l'exception Checked, comme cela a été fait en C #, comment peut-on alors transmettre par programme et structure la possibilité d'erreur? Comment informer le code client que telles et telles erreurs peuvent survenir et doivent être traitées?

J'entends toutes sortes d'horreurs lorsqu'il s'agit d'exceptions vérifiées, elles sont mal utilisées, c'est certain, mais les exceptions non contrôlées le sont aussi. Je dis attendre quelques années lorsque les API sont empilées sur plusieurs couches et que vous implorerez le retour d'une sorte de moyen structuré pour transmettre les échecs.

Prenons le cas lorsque l'exception a été levée quelque part au bas des couches API et vient de se propager parce que personne ne savait qu'il était même possible que cette erreur se produise, ceci même s'il s'agissait d'un type d'erreur très plausible lorsque le code appelant l'a jeté (FileNotFoundException par exemple par opposition à VogonsTrashingEarthExcept ... auquel cas cela n'aurait pas d'importance si nous le gérons ou non car il n'y a plus rien pour le gérer).

Beaucoup ont soutenu que ne pas pouvoir charger le fichier était presque toujours la fin du monde pour le processus et qu'il devait mourir d'une mort horrible et douloureuse. Alors oui ... bien sûr ... ok .. vous construisez une API pour quelque chose et il charge le fichier à un moment donné ... Moi, en tant qu'utilisateur de ladite API, je ne peux que répondre ... "Qui diable êtes-vous pour décider quand mon programme devrait planter! " Bien sûr, étant donné le choix où les exceptions sont englouties et ne laissent aucune trace ou l'exception EletroFlabbingChunkFluxManifoldChuggingException avec une trace de pile plus profonde que la tranchée Marianna, je prendrais cette dernière sans une pincée d'hésitation, mais cela signifie-t-il que c'est la façon souhaitable de traiter l'exception ? Ne pouvons-nous pas être quelque part au milieu, où l'exception serait refondue et enveloppée à chaque fois qu'elle traverserait un nouveau niveau d'abstraction pour que cela signifie réellement quelque chose?

Enfin, la plupart de l'argument que je vois est "Je ne veux pas traiter des exceptions, beaucoup de gens ne veulent pas traiter des exceptions. Les exceptions vérifiées m'obligent à les traiter ainsi je déteste les exceptions vérifiées" Pour éliminer complètement ce mécanisme et le reléguer au gouffre de l'enfer est tout simplement idiot et manque de jugement et de vision.

Si nous éliminons l'exception vérifiée, nous pourrions également éliminer le type de retour pour les fonctions et toujours renvoyer une variable "anytype" ... Cela simplifierait la vie maintenant, n'est-ce pas?

Newtopian
la source
2
Les exceptions vérifiées seraient utiles s'il existait un moyen déclaratif de dire qu'aucun des appels de méthode dans un bloc ne devrait déclencher certaines (ou aucune) exceptions vérifiées, et de telles exceptions devraient automatiquement être encapsulées et renvoyées. Ils pourraient être encore plus utiles si les appels à des méthodes déclarées comme levant des exceptions vérifiées échangeaient la vitesse / retour d'appel pour la vitesse de gestion des exceptions (afin que les exceptions attendues puissent être traitées presque aussi rapidement que le flux de programme normal). Cependant, aucune des deux situations ne s'applique actuellement.
supercat
6

En effet, les exceptions vérifiées d'une part augmentent la robustesse et l'exactitude de votre programme (vous êtes obligé de faire des déclarations correctes de vos interfaces - les exceptions qu'une méthode lève sont fondamentalement un type de retour spécial). D'un autre côté, vous êtes confronté au problème que, puisque les exceptions "bouillonnent", vous devez très souvent changer beaucoup de méthodes (tous les appelants et les appelants des appelants, etc.) lorsque vous modifiez une exception. jette la méthode.

Les exceptions vérifiées en Java ne résolvent pas ce dernier problème; C # et VB.NET jettent le bébé avec l'eau du bain.

Une belle approche qui prend la route du milieu est décrite dans ce document OOPSLA 2005 (ou le rapport technique connexe .)

En bref, il vous permet de dire:, method g(x) throws like f(x)ce qui signifie que g lève toutes les exceptions f lève. Voila, vérifié les exceptions sans le problème des changements en cascade.

Bien qu'il s'agisse d'un article académique, je vous encourage à le lire (en partie), car il explique bien les avantages et les inconvénients des exceptions vérifiées.

Kurt Schelfthout
la source
5

Ce n'est pas un argument contre le concept pur d'exceptions vérifiées, mais la hiérarchie de classes que Java utilise pour elles est un spectacle bizarre. Nous appelons toujours les choses simplement «exceptions» - ce qui est correct, car la spécification du langage les appelle aussi - mais comment une exception est-elle nommée et représentée dans le système de types?

Par la classe Exceptionqu'on imagine? Eh bien non, parce que Exceptions sont des exceptions, et de même les exceptions sont Exceptions, à l'exception des exceptions qui ne sont pasException s, car d'autres exceptions sont en fait des Errors, qui sont l'autre type d'exception, une sorte d'exception extra-exceptionnelle qui ne devrait jamais se produire sauf quand il le fait, et que vous ne devriez jamais attraper, sauf parfois. Sauf que ce n'est pas tout, car vous pouvez également définir d'autres exceptions qui ne sont ni Exceptions ni Errors mais simplement des Throwableexceptions.

Quelles sont les exceptions "vérifiées"? Throwableles s sont des exceptions vérifiées, sauf s'il s'agit également de Errors, qui sont des exceptions non contrôlées, et puis il y a les Exceptions, qui sont également des Throwables et sont le type principal d'exception vérifiée, sauf qu'il y a une exception à cela aussi, qui est que s'ils sont également RuntimeExceptions, car c'est l'autre type d'exception non contrôlée.

À quoi servent les RuntimeExceptions? Eh bien, comme son nom l'indique, ce sont des exceptions, comme tous les Exceptions, et elles se produisent au moment de l'exécution, comme toutes les exceptions en fait, sauf que les RuntimeExceptions sont exceptionnelles par rapport aux autres s d'exécution Exceptioncar elles ne sont pas censées se produire sauf lorsque vous faites une erreur stupide, bien que les RuntimeExceptions ne soient jamais des Errors, ils sont donc pour des choses exceptionnellement erronées mais qui ne sont pas réellement des Errors. Sauf pour RuntimeErrorException, qui est vraiment un RuntimeExceptionpour l' Errorart. Mais toutes les exceptions ne sont-elles pas censées représenter des circonstances erronées de toute façon? Oui, tous. À l'exception de ThreadDeath, une exception exceptionnellement exceptionnelle, car la documentation explique qu'il s'agit d'une «occurrence normale» et que «Error

Quoi qu'il en soit, puisque nous divisons toutes les exceptions au milieu en Errors (qui sont pour les exceptions d'exécution exceptionnelles, donc non cochées) et Exceptions (qui sont pour les erreurs d'exécution moins exceptionnelles, donc vérifiées sauf lorsqu'elles ne le sont pas), nous avons maintenant besoin de deux différents types de chacune de plusieurs exceptions. Nous avons donc besoin de IllegalAccessErroret IllegalAccessException, et InstantiationErroret InstantiationException, et NoSuchFieldErroret NoSuchFieldException, et NoSuchMethodErroret NoSuchMethodException, et ZipErroret ZipException.

Sauf que même lorsqu'une exception est vérifiée, il existe toujours des moyens (assez faciles) de tromper le compilateur et de le lancer sans qu'il soit vérifié. Si vous vous faites, vous pouvez obtenir un UndeclaredThrowableException, sauf dans d'autres cas, où il pourrait apparaître comme un UnexpectedException, ou un UnknownException(qui n'est pas lié à UnknownError, ce qui n'est que pour des "exceptions graves"), ou un ExecutionException, ou un InvocationTargetException, ou un ExceptionInInitializerError.

Oh, et nous ne devons pas oublier le nouveau snazzy de Java 8 UncheckedIOException, qui est une RuntimeExceptionexception conçue pour vous permettre de jeter le concept de vérification des exceptions par la fenêtre en enveloppant les IOExceptionexceptions vérifiées causées par des erreurs d'E / S (qui ne provoquent pas d' IOErrorexceptions, bien que cela existe aussi) qui sont exceptionnellement difficiles à manipuler et vous avez donc besoin qu'elles ne soient pas vérifiées.

Merci Java!

Boann
la source
2
Pour autant que je puisse dire, cette réponse ne dit que "les exceptions de Java sont un gâchis" d'une manière pleine d'ironie, sans doute drôle. Ce qu'il ne semble pas faire, c'est expliquer pourquoi les programmeurs ont tendance à éviter d'essayer de comprendre comment ces choses sont censées fonctionner. De plus, dans des cas réels (du moins ceux avec lesquels j'ai eu la chance de traiter), si les programmeurs n'essaient pas délibérément de rendre leur vie plus difficile, les exceptions sont loin d'être aussi compliquées que vous l'avez décrit.
CptBartender
5

Le problème

Le pire problème que je vois avec le mécanisme de gestion des exceptions est qu'il introduit une duplication de code à grande échelle ! Soyons honnêtes: dans la plupart des projets dans 95% du temps, tout ce que les développeurs doivent vraiment faire, sauf exception, est de le communiquer d'une manière ou d'une autre à l'utilisateur (et, dans certains cas, à l'équipe de développement également, par exemple en envoyant un e -mail avec la trace de la pile). Donc, généralement, la même ligne / bloc de code est utilisé à chaque endroit où l'exception est gérée.

Supposons que nous enregistrions simplement chaque bloc catch pour un certain type d'exception vérifiée:

try{
   methodDeclaringCheckedException();
}catch(CheckedException e){
   logger.error(e);
}

S'il s'agit d'une exception courante, il peut y avoir même plusieurs centaines de ces blocs try-catch dans une base de code plus grande. Supposons maintenant que nous devons introduire une gestion des exceptions basée sur une boîte de dialogue contextuelle au lieu de la journalisation de la console ou commencer à envoyer un e-mail à l'équipe de développement.

Attendez un instant ... allons-nous vraiment modifier tout cela plusieurs centaines d'emplacements dans le code?! Vous obtenez mon point :-).

La solution

Ce que nous avons fait pour résoudre ce problème a été d'introduire le concept de gestionnaires d'exceptions (auquel je ferai référence plus loin sous le nom d'EH) pour centraliser la gestion des exceptions. Pour chaque classe qui a besoin de suspendre des exceptions, une instance de gestionnaire d'exceptions est injectée par notre infrastructure d'injection de dépendances . Ainsi, le modèle typique de gestion des exceptions ressemble maintenant à ceci:

try{
    methodDeclaringCheckedException();
}catch(CheckedException e){
    exceptionHandler.handleError(e);
}

Maintenant, pour personnaliser notre gestion des exceptions, il nous suffit de changer le code en un seul endroit (code EH).

Bien sûr, pour les cas plus complexes, nous pouvons implémenter plusieurs sous-classes d'EH et tirer parti des fonctionnalités que notre infrastructure DI nous fournit. En modifiant la configuration de notre infrastructure DI, nous pouvons facilement basculer l'implémentation EH à l'échelle mondiale ou fournir des implémentations spécifiques d'EH à des classes ayant des besoins de gestion des exceptions spécifiques (par exemple en utilisant l'annotation Guice @Named).

De cette façon, nous pouvons différencier le comportement de gestion des exceptions dans le développement et la version de publication de l'application (par exemple, développement - journalisation de l'erreur et arrêt de l'application, prod - journalisation de l'erreur avec plus de détails et laisser l'application continuer son exécution) sans effort.

Dernière chose

Enfin et surtout, il peut sembler que le même type de centralisation peut être obtenu en passant simplement nos exceptions "jusqu'à" jusqu'à ce qu'elles arrivent à une classe de gestion des exceptions de niveau supérieur. Mais cela conduit à l'encombrement du code et des signatures de nos méthodes et introduit des problèmes de maintenance mentionnés par d'autres dans ce fil.

Piotr Sobczyk
la source
6
Les exceptions sont inventées pour faire quelque chose d'utile avec elles. Les écrire dans un fichier journal ou rendre une jolie fenêtre n'est pas utile, car le problème d'origine n'est pas résolu par cela. Faire quelque chose d'utile nécessite d'essayer une stratégie de solution différente. Exemples: si je ne parviens pas à obtenir mes données du serveur AI, essayez-les sur le serveur B. Ou si l'algorithme A produit un débordement de tas, j'essaie l'algorithme B qui est beaucoup plus lent mais peut réussir.
ceving
2
@ceving Oui, tout est bon et vrai en théorie. Mais maintenant, revenons à la pratique du mot. Veuillez répondre très honnêtement à quelle fréquence vous le faites dans votre projet réel? Quelle partie des catchblocs de ce projet réel fait quelque chose de vraiment "utile" avec les exceptions? 10% serait bien. Les problèmes habituels qui génèrent des exceptions sont comme essayer de lire la configuration à partir d'un fichier qui n'existe pas, OutOfMemoryErrors, NullPointerExceptions, les erreurs d'intégrité des contraintes de base de données, etc., etc. Essayez-vous vraiment de récupérer gracieusement à partir de tous? Je ne te crois pas :). Souvent, il n'y a tout simplement aucun moyen de récupérer.
Piotr Sobczyk
3
@PiotrSobczyk: Si un programme prend des mesures à la suite d'une demande suer et que l'opération échoue d'une manière qui n'a rien endommagé dans l'état du système, notifier à l'utilisateur que l'opération n'a pas pu être terminée est parfaitement utile manière de gérer la situation. Le plus grand échec des exceptions en C # et .net est qu'il n'y a aucun moyen cohérent de vérifier si quelque chose dans l'état du système a pu être endommagé.
supercat
4
Correct, @PiotrSobczyk. Le plus souvent, la seule action correcte à entreprendre en réponse à une exception est d'annuler la transaction et de renvoyer une réponse d'erreur. Les idées de «résolution d'exception» impliquent des connaissances et une autorité que nous n'avons pas (et ne devrions pas) avoir et violent l'encapsulation. Si notre application n'est pas une base de données, nous ne devrions pas essayer de réparer la base de données. Échouer proprement et éviter d'écrire des données erronées, ceving, est suffisamment utile .
Thomas W
@PiotrSobczyk Hier, j'ai traité une exception "impossible de lire l'objet" (qui ne se produira que parce que la base de données sous-jacente a été mise à jour avant le logiciel - ce qui ne devrait jamais se produire mais est une possibilité due à une erreur humaine) en basculant vers un version historique de la base de données garantie pour pointer vers une ancienne version de l'objet.
Éponyme du
4

Anders parle des pièges des exceptions vérifiées et pourquoi il les a laissés hors de C # dans l' épisode 97 de la radio Software Engineering.

Chuck Conway
la source
4

Ma rédaction sur c2.com est toujours pratiquement inchangée par rapport à sa forme d'origine: CheckedExceptionsAreIncompatibleWithVisitorPattern

En résumé:

Le modèle de visiteur et ses parents sont une classe d'interfaces où l'appelant indirect et l'implémentation d'interface connaissent tous deux une exception mais l'interface et l'appelant direct forment une bibliothèque qui ne peut pas savoir.

L'hypothèse fondamentale de CheckedExceptions est que toutes les exceptions déclarées peuvent être levées à partir de n'importe quel point qui appelle une méthode avec cette déclaration. Le VisitorPattern révèle que cette hypothèse est erronée.

Le résultat final des exceptions vérifiées dans des cas comme ceux-ci est beaucoup de code autrement inutile qui supprime essentiellement la contrainte d'exception vérifiée du compilateur lors de l'exécution.

Quant au problème sous-jacent:

Mon idée générale est que le gestionnaire de niveau supérieur doit interpréter l'exception et afficher un message d'erreur approprié. Je vois presque toujours des exceptions d'E / S, des exceptions de communication (pour une raison quelconque, les API distinguent) ou des erreurs fatales pour les tâches (bogues de programme ou problème grave sur le serveur de support), donc cela ne devrait pas être trop difficile si nous autorisons une trace de pile pour une grave problème de serveur.

Joshua
la source
1
Vous devriez avoir quelque chose comme DAGNodeException dans l'interface, puis intercepter l'IOException et le convertir en DAGNodeException: l'appel public void (DAGNode arg) lève DAGNodeException;
TofuBeer
2
@TofuBeer, c'est exactement mon point. Je trouve que l'encapsulation et le déroulement constants des exceptions sont pires que la suppression des exceptions vérifiées.
Joshua
2
Eh bien, nous ne sommes pas du tout d'accord alors ... mais votre article ne répond toujours pas à la véritable question sous-jacente de savoir comment empêcher votre application d'afficher une trace de pile à l'utilisateur lorsqu'une exception d'exécution est levée.
TofuBeer
1
@TofuBeer - Quand il échoue, dire à l'utilisateur qu'il a échoué est correct! Quelle est votre alternative, autre que de "passer outre" l'échec avec des données "nulles" ou incomplètes / incorrectes? Faire semblant d'avoir réussi est un mensonge, qui ne fait qu'empirer les choses. Avec 25 ans d'expérience dans les systèmes à haute fiabilité, la logique de nouvelle tentative ne doit être utilisée qu'avec prudence et lorsque cela est approprié. Je m'attendrais également à ce qu'un visiteur échoue à nouveau, peu importe combien de fois vous le réessayez. Sauf si vous pilotez un avion, le passage à une deuxième version du même algorithme est peu pratique et invraisemblable (et peut échouer de toute façon).
Thomas W
4

Pour tenter de répondre uniquement à la question sans réponse:

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

La question contient un raisonnement spécieux à mon humble avis. Ce n'est pas parce que l'API vous dit ce qu'elle lance que vous la traitez de la même manière dans tous les cas. En d'autres termes, les exceptions que vous devez intercepter varient en fonction du contexte dans lequel vous utilisez le composant lançant l'exception.

Par exemple:

Si j'écris un testeur de connexion pour une base de données ou quelque chose pour vérifier la validité d'un XPath entré par l'utilisateur, je voudrais probablement attraper et signaler toutes les exceptions vérifiées et non vérifiées qui sont levées par l'opération.

Cependant, si j'écris un moteur de traitement, je traiterai probablement une XPathException (cochée) de la même manière qu'un NPE: je la laisserais remonter jusqu'en haut du thread de travail, ignorer le reste de ce lot, enregistrer le problème (ou l'envoyer à un service d'assistance pour diagnostic) et laisser des commentaires pour que l'utilisateur puisse contacter l'assistance.

Dave Elton
la source
2
Exactement. Facile et direct, la façon dont la gestion des exceptions est. Comme le dit Dave, la gestion correcte des exceptions se fait normalement à un niveau élevé . "Lancer tôt, attraper tard" est le principe. Les exceptions vérifiées rendent cela difficile.
Thomas W
4

Cet article est le meilleur texte que j'ai jamais lu sur la gestion des exceptions en Java.

Il privilégie les exceptions non contrôlées par rapport aux exceptions vérifiées, mais ce choix est expliqué très attentivement et sur la base d'arguments solides.

Je ne veux pas citer trop le contenu de l'article ici (il est préférable de le lire dans son ensemble) mais il couvre la plupart des arguments des défenseurs des exceptions non vérifiées de ce fil. Cet argument (qui semble assez populaire) est particulièrement couvert:

Prenons le cas lorsque l'exception a été levée quelque part au bas des couches API et vient de se propager parce que personne ne savait qu'il était même possible que cette erreur se produise, ceci même s'il s'agissait d'un type d'erreur très plausible lorsque le code appelant l'a jeté (FileNotFoundException par exemple par opposition à VogonsTrashingEarthExcept ... auquel cas cela n'aurait pas d'importance si nous le gérons ou non car il n'y a plus rien pour le gérer).

L'auteur "réponses":

Il est absolument incorrect de supposer que toutes les exceptions d'exécution ne doivent pas être interceptées et autorisées à se propager au "sommet" de l'application. (...) Pour chaque condition exceptionnelle qui doit être traitée distinctement - par les exigences du système / de l'entreprise - les programmeurs doivent décider où l'attraper et quoi faire une fois que la condition est détectée. Cela doit être fait strictement en fonction des besoins réels de l'application, et non sur la base d'une alerte du compilateur. Toutes les autres erreurs doivent être autorisées à se propager librement vers le gestionnaire le plus haut où elles seraient enregistrées et une action gracieuse (peut-être une interruption) sera prise.

Et la pensée ou l'article principal est:

En ce qui concerne la gestion des erreurs dans les logiciels, la seule hypothèse sûre et correcte qui puisse être faite est qu'une défaillance peut se produire dans absolument tous les sous-programmes ou modules qui existent!

Donc, si " personne ne savait qu'il était même possible que cette erreur se produise ", il y a un problème avec ce projet. Une telle exception doit être gérée par au moins le gestionnaire d'exceptions le plus générique (par exemple, celui qui gère toutes les exceptions non gérées par des gestionnaires plus spécifiques) comme le suggère l'auteur.

Tellement triste peu de gens semblent découvrir ce grand article :-(. Je recommande de tout cœur à tous ceux qui hésitent quelle approche est préférable de prendre un peu de temps et de la lire.

Piotr Sobczyk
la source
4

Les exceptions vérifiées étaient, dans leur forme originale, une tentative de gérer les imprévus plutôt que les échecs. L'objectif louable était de mettre en évidence des points spécifiques prévisibles (impossible de se connecter, fichier introuvable, etc.) et de garantir que les développeurs les gèrent.

Ce qui n'a jamais été inclus dans le concept d'origine, a été de forcer la déclaration d'une vaste gamme de défaillances systémiques et irrécupérables. Ces échecs n'étaient jamais corrects pour être déclarés comme exceptions vérifiées.

Les échecs sont généralement possibles dans le code, et les conteneurs EJB, web et Swing / AWT répondent déjà à cela en fournissant un gestionnaire d'exceptions de «demande ayant échoué» le plus à l'extérieur. La stratégie correcte la plus élémentaire consiste à annuler la transaction et à renvoyer une erreur.

Un point crucial est que les exceptions d'exécution et vérifiées sont fonctionnellement équivalentes. Il n'y a aucune manipulation ou récupération que les exceptions vérifiées peuvent faire, contrairement aux exceptions d'exécution.

Le plus grand argument contre les exceptions «vérifiées» est que la plupart des exceptions ne peuvent pas être corrigées. Le simple fait est que nous ne possédons pas le code / sous-système qui s'est cassé. Nous ne pouvons pas voir l'implémentation, nous n'en sommes pas responsables et ne pouvons pas la corriger.

Si notre application n'est pas une base de données .. nous ne devrions pas essayer de réparer la base de données. Cela violerait le principe de l'encapsulation .

Les domaines JDBC (SQLException) et RMI pour EJB (RemoteException) ont été particulièrement problématiques. Plutôt que d'identifier les éventualités pouvant être résolues conformément au concept original d '«exception vérifiée», ces problèmes de fiabilité systémique omniprésents, qui ne sont pas réellement réparables, doivent être largement déclarés.

L'autre grave faille de la conception Java est que la gestion des exceptions doit être correctement placée au niveau "métier" ou "demande" le plus élevé possible. Le principe ici est "lancer tôt, attraper tard". Les exceptions vérifiées ne font pas grand chose, mais gênent cela.

Nous avons un problème évident en Java d'exiger des milliers de blocs try-catch ne rien faire, avec une proportion importante (40% +) mal codée. Presque aucun de ceux-ci ne met en œuvre une véritable manipulation ou fiabilité, mais impose des frais généraux de codage importants.

Enfin, les "exceptions vérifiées" sont à peu près incompatibles avec la programmation fonctionnelle FP.

Leur insistance sur le «traitement immédiat» est en contradiction avec les meilleures pratiques de «gestion tardive» de la gestion des exceptions et avec toute structure FP qui résume les boucles / ou le flux de contrôle.

Beaucoup de gens parlent de "gérer" les exceptions vérifiées, mais parlent à travers leurs chapeaux. Continuer après un échec avec des données nulles, incomplètes ou incorrectes pour faire semblant de succès ne gère rien. C'est faute professionnelle d'ingénierie / fiabilité de la forme la plus basse.

À défaut, c'est la stratégie correcte la plus élémentaire pour gérer une exception. Annuler la transaction, consigner l'erreur et signaler une réponse «échec» à l'utilisateur est une bonne pratique - et surtout, empêche la validation de données commerciales incorrectes dans la base de données.

D'autres stratégies de gestion des exceptions sont "réessayer", "reconnecter" ou "ignorer", au niveau de l'entreprise, du sous-système ou de la demande. Tous ces éléments sont des stratégies de fiabilité générales et fonctionnent bien / mieux avec des exceptions d'exécution.

Enfin, il est de loin préférable d’échouer que d’exécuter avec des données incorrectes. Poursuivre entraînera soit des erreurs secondaires, éloignées de la cause d'origine et plus difficiles à déboguer; ou entraînera éventuellement la validation de données erronées. Les gens se font virer pour ça.

Voir:
- http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/

Thomas W
la source
1
Mon point est d' échouer correctement en tant que stratégie générale. Les exceptions non vérifiées aident car elles n'obligent pas à interposer les blocs catch. Les captures et la journalisation des erreurs peuvent ensuite être laissées à quelques gestionnaires les plus externes, plutôt que mal codées des milliers de fois dans la base de code (qui est en fait ce qui cache les bogues) . Pour les échecs arbitraires, les exceptions non vérifiées sont absolument les plus correctes. Les éventualités - des résultats prévisibles tels que des fonds insuffisants - sont les seules exceptions qui méritent légitimement d'être vérifiées.
Thomas W
1
Ma réponse déjà ci-dessus répond à cela. D'abord et avant tout, 1) Le gestionnaire d'échec le plus externe doit tout attraper. Au-delà de cela, pour des sites spécifiques identifiés uniquement, 2) Des contingences spécifiques attendues peuvent être capturées et traitées - sur le site immédiat où elles sont lancées. Cela signifie que le fichier n'est pas trouvé, les fonds insuffisants, etc. au moment où ceux-ci pourraient être récupérés - pas plus. Le principe d'encapsulation signifie que les couches externes ne peuvent / ne doivent pas être responsables de comprendre / récupérer des défaillances profondes à l'intérieur. Troisièmement, 3) Tout le reste doit être jeté vers l'extérieur - décoché si possible.
Thomas W
1
Le gestionnaire le plus à l'extérieur intercepte l'exception, l' enregistre et renvoie une réponse «échoué» ou affiche une boîte de dialogue d'erreur. Très simple, pas difficile à définir du tout. Le fait est que chaque exception qui n'est pas immédiatement et localement récupérable, est une défaillance irrécupérable due au principe de l'encapsulation. Si le code qui est censé être connu ne peut pas le récupérer, alors la demande échoue globalement proprement et correctement. C'est la bonne façon de le faire correctement.
Thomas W
1
Incorrect. Le travail du gestionnaire le plus externe consiste à échouer proprement et à consigner les erreurs à la limite de la «demande». La demande cassée échoue correctement, une exception est signalée, le thread est en mesure de continuer à traiter la demande suivante. Un tel gestionnaire externe est une fonctionnalité standard dans les conteneurs Tomcat, AWT, Spring, EJB et le thread principal Java.
Thomas W
1
Pourquoi est-il dangereux de signaler des "bogues authentiques" à la limite de la demande ou au gestionnaire externe ??? Je travaille fréquemment dans l'intégration et la fiabilité des systèmes, où une ingénierie de fiabilité correcte est réellement importante, et j'utilise pour cela des approches "d'exception non vérifiée". Je ne suis pas vraiment sûr de ce que vous débattez réellement - il semble que vous souhaitiez peut-être passer 3 mois de manière non contrôlée, avoir une idée de cela, puis nous pourrons peut-être en discuter davantage. Merci.
Thomas W
4

Comme les gens l'ont déjà dit, les exceptions vérifiées n'existent pas dans le bytecode Java. Ils sont simplement un mécanisme de compilation, un peu comme les autres vérifications de syntaxe. Je vois vérifié exceptions beaucoup comme je vois le compilateur se plaindre d'un conditionnel redondant: if(true) { a; } b;. C'est utile, mais j'aurais pu le faire exprès, alors laissez-moi ignorer vos avertissements.

Le fait est que vous ne pourrez pas forcer chaque programmeur à "faire la bonne chose" si vous appliquez des exceptions vérifiées et que tout le monde est maintenant un dommage collatéral qui vous déteste pour la règle que vous avez établie.

Réparez les mauvais programmes! N'essayez pas de corriger la langue pour ne pas les autoriser! Pour la plupart des gens, «faire quelque chose à propos d'une exception» revient vraiment à en informer l'utilisateur. Je peux également informer l'utilisateur d'une exception non vérifiée, alors gardez vos classes d'exceptions vérifiées hors de mon API.

Martin
la source
À droite, je voulais juste souligner la différence entre le code inaccessible (qui génère une erreur) et les conditions avec un résultat prévisible. Je supprimerai ce commentaire plus tard.
Holger
3

Un problème avec les exceptions vérifiées est que les exceptions sont souvent attachées aux méthodes d'une interface si même une implémentation de cette interface l'utilise.

Un autre problème avec les exceptions vérifiées est qu'elles ont tendance à être mal utilisées. L'exemple parfait de ceci est dans java.sql.Connectionla close()méthode de. Il peut lancer un SQLException, même si vous avez déjà explicitement déclaré que vous avez terminé avec la connexion. Quelles informations pourraient fermer () éventuellement transmettre qui vous intéressent?

Habituellement, lorsque je ferme () une connexion *, cela ressemble à ceci:

try {
    conn.close();
} catch (SQLException ex) {
    // Do nothing
}

De plus, ne me lancez pas sur les différentes méthodes d'analyse et NumberFormatException ... TryParse de .NET, qui ne lève pas d'exceptions, est tellement plus facile à utiliser qu'il est pénible de devoir revenir à Java (nous utilisons à la fois Java et C # où je travaille).

*En tant que commentaire supplémentaire, Connection.close () de PooledConnection ne ferme même pas une connexion, mais vous devez toujours intercepter l'exception SQLException car il s'agit d'une exception vérifiée.

Powerlord
la source
2
Bon, n'importe lequel des pilotes peut ... la question est plutôt "pourquoi le programmeur devrait-il s'en soucier?" car il a quand même fini d'accéder à la base de données. Les documents vous avertissent même que vous devez toujours valider () ou annuler () la transaction en cours avant d'appeler close ().
Powerlord
Beaucoup de gens pensent que fermer un fichier ne peut pas lever d'exception ... stackoverflow.com/questions/588546/… êtes-vous à 100% certain qu'il n'y a pas de cas que cela importerait?
TofuBeer
Je suis certain à 100% qu'il n'y a pas de cas qu'il serait question et que l'appelant ne serait pas mis dans un try / catch.
Martin
1
Excellent exemple avec la fermeture des connexions, Martin! Je ne peux que vous reformuler: si nous expliquons simplement que nous avons terminé avec une connexion, pourquoi devrait-on se préoccuper de ce qui se passe lorsque nous le fermons. Il y a plus de cas comme celui-ci que le programmeur ne se soucie pas vraiment si une exception se produit et il a absolument raison.
Piotr Sobczyk
1
@PiotrSobczyk: Certains pilotes SQL vont squawk si l'on ferme une connexion après avoir démarré une transaction mais sans la confirmer ni l'annuler. À mon humble avis, le squawking est mieux que d'ignorer silencieusement le problème, au moins dans les cas où le squawking ne fera pas perdre d'autres exceptions.
supercat
3

Le programmeur doit connaître toutes les exceptions qu'une méthode peut déclencher, afin de l'utiliser correctement. Donc, le battre au-dessus de la tête avec seulement quelques-unes des exceptions n'aide pas nécessairement un programmeur imprudent à éviter les erreurs.

L'avantage mince est contrebalancé par le coût onéreux (en particulier dans les bases de code plus grandes et moins flexibles où la modification constante des signatures d'interface n'est pas pratique).

L'analyse statique peut être agréable, mais une analyse statique vraiment fiable requiert souvent un travail rigoureux de la part du programmeur. Il existe un calcul coûts-avantages et la barre doit être placée haute pour une vérification qui conduit à une erreur de temps de compilation. Il serait plus utile que l'EDI joue le rôle de communiquer les exceptions qu'une méthode peut lancer (y compris celles qui sont inévitables). Bien que ce ne soit peut-être pas aussi fiable sans déclarations d'exceptions forcées, la plupart des exceptions seraient toujours déclarées dans la documentation, et la fiabilité d'un avertissement IDE n'est pas si cruciale.

Aleksandr Dubinsky
la source
2

Je pense que c'est une excellente question et pas du tout argumentative. Je pense que les bibliothèques tierces devraient (en général) lever des exceptions non contrôlées . Cela signifie que vous pouvez isoler vos dépendances sur la bibliothèque (c'est-à-dire que vous n'avez pas besoin de relancer leurs exceptions ou de lever Exception- généralement une mauvaise pratique). La couche DAO de Spring en est un excellent exemple.

D'un autre côté, les exceptions de l'API Java de base doivent en général être vérifiées si elles peuvent jamais être gérées. Prenez FileNotFoundExceptionou (mon préféré) InterruptedException. Ces conditions doivent presque toujours être traitées de manière spécifique (c'est-à-dire que votre réaction à un InterruptedExceptionn'est pas la même que votre réaction à un IllegalArgumentException). Le fait que vos exceptions soient vérifiées oblige les développeurs à se demander si une condition est gérable ou non. (Cela dit, je l'ai rarement vu InterruptedExceptioncorrectement!)

Une dernière chose - un RuntimeExceptionn'est pas toujours "où un développeur a quelque chose de mal". Une exception d'argument illégal est levée lorsque vous essayez de créer une enumutilisation valueOfet il n'y a pas enumde ce nom. Ce n'est pas nécessairement une erreur du développeur!

oxbow_lakes
la source
1
Oui, c'est une erreur du développeur. Ils n'ont clairement pas utilisé le bon nom, ils doivent donc revenir en arrière et corriger leur code.
AxiomaticNexus
@AxiomaticNexus Aucun développeur sensé n'utilise de enumnoms de membres, simplement parce qu'ils utilisent des enumobjets à la place. Un mauvais nom ne peut donc provenir que de l'extérieur, que ce soit un fichier d'importation ou autre. Une façon possible de gérer ces noms est d'appeler MyEnum#valueOfet d'attraper l'IAE. Une autre façon consiste à utiliser un pré-rempli Map<String, MyEnum>, mais ce sont des détails d'implémentation.
maaartinus
@maaartinus Dans certains cas, les noms de membres enum sont utilisés sans que la chaîne ne vienne de l'extérieur. Par exemple, lorsque vous souhaitez parcourir tous les membres de manière dynamique pour faire quelque chose avec chacun. De plus, que la chaîne vienne de l'extérieur ou non n'a pas d'importance. Le développeur dispose de toutes les informations dont il a besoin pour savoir si le passage de la chaîne x à "MyEnum # valueOf" entraînera une erreur avant de la transmettre. Passer la chaîne x à "MyEnum # valueOf" de toute façon quand cela aurait causé une erreur, serait clairement une erreur de la part du développeur.
AxiomaticNexus
2

Voici un argument contre les exceptions vérifiées (de joelonsoftware.com):

Le raisonnement est que je considère que les exceptions ne valent pas mieux que les «goto», considérées comme nuisibles depuis les années 1960, en ce qu'elles créent un saut brutal d'un point de code à un autre. En fait, ils sont bien pires que ceux de goto:

  • Ils sont invisibles dans le code source. En regardant un bloc de code, y compris des fonctions qui peuvent ou non lever des exceptions, il n'y a aucun moyen de voir quelles exceptions peuvent être levées et d'où. Cela signifie que même une inspection minutieuse du code ne révèle pas de bogues potentiels.
  • Ils créent trop de points de sortie possibles pour une fonction. Pour écrire du code correct, vous devez vraiment penser à chaque chemin de code possible à travers votre fonction. Chaque fois que vous appelez une fonction qui peut déclencher une exception et ne la détecte pas immédiatement, vous créez des opportunités de bugs surprise causés par des fonctions qui se sont arrêtées brutalement, laissant les données dans un état incohérent ou d'autres chemins de code que vous n'avez pas Penser à.
finnw
la source
3
+1 Vous voudrez peut-être résumer l'argument dans votre réponse? Ils sont comme des gotos invisibles et des sorties anticipées pour vos routines, dispersés tout au long du programme.
MarkJ
13
C'est plus un argument contre les exceptions en général.
Ionuț G. Stan
10
avez-vous réellement lu l'article !! D'une part, il parle des exceptions en général, d'autre part la section "Elles sont invisibles dans le code source" s'applique spécifiquement à l'exception UNCHECKED. C'est tout le point de l'exception vérifiée ... pour que vous SAVIEZ quel code lève quoi où
Newtopian
2
@Eva Ce ne sont pas les mêmes. Avec une instruction goto, vous pouvez voir le gotomot - clé. Avec une boucle, vous pouvez voir l'accolade de fermeture ou le mot break- continueclé ou . Tous sautent à un point dans la méthode actuelle. Mais vous ne pouvez pas toujours voir le throw, car souvent ce n'est pas dans la méthode actuelle mais dans une autre méthode qu'il appelle (éventuellement indirectement.)
fin
5
@finnw Les fonctions sont elles-mêmes une forme de goto. Vous ne savez généralement pas quelles fonctions les fonctions que vous appelez appellent. Si vous programmiez sans fonctions, vous n'auriez pas de problème avec des exceptions invisibles. Ce qui signifie que le problème n'est pas spécifiquement lié aux exceptions et n'est pas un argument valable contre les exceptions en général. Vous pourriez dire que les codes d'erreur sont plus rapides, vous pourriez dire que les monades sont plus propres, mais l'argument goto est stupide.
Eva
2

Le bon prouve que l'exception vérifiée n'est pas nécessaire:

  1. Beaucoup de frameworks qui fonctionnent pour Java. Comme Spring qui encapsule l'exception JDBC dans les exceptions non contrôlées, lançant des messages dans le journal
  2. Beaucoup de langues qui sont venues après Java, même en haut sur la plate-forme Java - elles ne les utilisent pas
  3. Exceptions vérifiées, c'est une sorte de prédiction sur la façon dont le client utiliserait le code qui lève une exception. Mais un développeur qui écrit ce code ne connaîtra jamais le système et l'entreprise dans lesquels travaille le client du code. Par exemple, les méthodes d'interface qui forcent à lever l'exception vérifiée. Il y a 100 implémentations sur le système, 50 ou même 90 implémentations ne lèvent pas cette exception, mais le client doit toujours intercepter cette exception s'il fait référence à cette interface par l'utilisateur. Ces 50 ou 90 implémentations ont tendance à gérer ces exceptions à l'intérieur de lui-même, en mettant une exception au journal (et c'est un bon comportement pour eux). Que devons-nous en faire? Je ferais mieux d'avoir une logique de fond qui ferait tout ce travail - envoyer un message au journal. Et si moi, en tant que client de code, je ressentais le besoin de gérer l'exception - je le ferai.
  4. Un autre exemple lorsque je travaille avec des E / S en java, cela m'oblige à vérifier toutes les exceptions, si le fichier n'existe pas? que dois-je faire avec ça? S'il n'existe pas, le système ne passerait pas à l'étape suivante. Le client de cette méthode n'obtiendrait pas le contenu attendu de ce fichier - il peut gérer l'exception d'exécution, sinon je devrais d'abord vérifier l'exception vérifiée, mettre un message dans le journal, puis lever l'exception à partir de la méthode. Non ... non - je ferais mieux de le faire automatiquement avec RuntimeEception, qui le fait / s'allume automatiquement. Il n'y a aucun sens à le gérer manuellement - je serais heureux d'avoir vu un message d'erreur dans le journal (AOP peut aider à cela .. quelque chose qui corrige java). Si, finalement, je pense que le système devrait afficher un message contextuel à l'utilisateur final - je le montrerai, pas un problème.

J'étais heureux que java me donne le choix de ce que j'utiliserais lorsque je travaillerais avec des bibliothèques de base, comme les E / S. Comme fournit deux copies des mêmes classes - une enveloppée avec RuntimeEception. Ensuite, nous pouvons comparer ce que les gens utiliseraient . Pour l'instant, cependant, beaucoup de gens préfèrent opter pour un framework en haut sur java, ou un langage différent. Comme Scala, JRuby que ce soit. Beaucoup croient simplement que SUN avait raison.

ses
la source
1
Plutôt que d'avoir deux versions de classes, il devrait y avoir un moyen concis de spécifier qu'aucun des appels de méthode effectués par un bloc de code ne devrait déclencher d'exceptions de certains types, et que de telles exceptions devraient être encapsulées via certains moyens spécifiés et rethrown (par défaut, créez un nouveau RuntimeExceptionavec une exception interne appropriée). Il est malheureux qu'il soit plus concis d'avoir la méthode externe throwsune exception à la méthode interne, que de la faire envelopper les exceptions de la méthode interne, alors que la dernière ligne de conduite serait plus souvent correcte.
supercat
2

Une chose importante que personne n'a mentionnée est la façon dont il interfère avec les interfaces et les expressions lambda.

Disons que vous définissez un MyAppException extends Exception. Il s'agit de l'exception de niveau supérieur héritée par toutes les exceptions levées par votre application. Chaque méthode déclare throws MyAppExceptionce qui est un peu de nuissance, mais gérable. Un gestionnaire d'exceptions consigne l'exception et notifie l'utilisateur d'une manière ou d'une autre.

Tout semble OK jusqu'à ce que vous souhaitiez implémenter une interface qui n'est pas la vôtre. De toute évidence, il ne déclare pas l'intention de lancer MyApException, donc le compilateur ne vous permet pas de lever l'exception à partir de là.

Cependant, si votre exception s'étend RuntimeException, il n'y aura aucun problème avec les interfaces. Vous pouvez mentionner volontairement l'exception dans JavaDoc si vous le souhaitez. Mais à part cela, il ne fait que bouillonner silencieusement à travers quoi que ce soit, pour être pris dans votre couche de gestion des exceptions.

Vlasec
la source
1

Nous avons vu quelques références à l'architecte en chef de C #.

Voici un autre point de vue d'un type Java sur le moment d'utiliser les exceptions vérifiées. Il reconnaît bon nombre des points négatifs que d'autres ont mentionnés: Exceptions efficaces

Jacob Toronto
la source
2
Le problème avec les exceptions vérifiées en Java provient d'un problème plus profond, qui est que trop d'informations sont encapsulées dans le TYPE de l'exception, plutôt que dans les propriétés d'une instance. Il serait utile d'avoir vérifié les exceptions, si être "vérifié" était un attribut des sites throw / catch, et si l'on pouvait déclarer de manière déclarative si une exception vérifiée qui échappe à un bloc de code doit rester une exception vérifiée, ou être vue par tout bloc englobant comme exception non vérifiée; De même, les blocs catch devraient pouvoir spécifier qu'ils ne veulent que des exceptions vérifiées.
supercat
1
Supposons qu'une routine de recherche de dictionnaire soit spécifiée pour lever un type particulier d'exception si une tentative d'accès à une clé inexistante est effectuée. Il peut être raisonnable que le code client intercepte une telle exception. Si, cependant, une méthode utilisée par la routine de recherche arrive à lancer ce même type d'exception d'une manière à laquelle la routine de recherche ne s'attend pas, le code client ne devrait probablement pas l'attraper. La vérification de la propriété des instances d' exception , des sites de lancement et des sites de capture éviterait de tels problèmes. Le client intercepterait les exceptions «vérifiées» de ce type, en esquivant les exceptions inattendues.
supercat
0

J'ai lu beaucoup de choses sur la gestion des exceptions, même si (la plupart du temps) je ne peux pas vraiment dire que je suis heureux ou triste de l'existence d'exceptions vérifiées, c'est mon point de vue: les exceptions vérifiées dans le code de bas niveau (IO, mise en réseau , OS, etc.) et les exceptions non vérifiées dans les API / niveau d'application de haut niveau.

Même s'il n'est pas si facile de tracer une ligne entre eux, je trouve qu'il est vraiment ennuyeux / difficile d'intégrer plusieurs API / bibliothèques sous le même toit sans encapsuler tout le temps beaucoup d'exceptions vérifiées mais d'un autre côté, parfois est utile / préférable d'être obligé d'attraper une exception et d'en fournir une autre qui a plus de sens dans le contexte actuel.

Le projet sur lequel je travaille prend beaucoup de bibliothèques et les intègre sous la même API, API qui est entièrement basée sur des exceptions non vérifiées.Ces frameworks fournissent une API de haut niveau qui au début était pleine d'exceptions vérifiées et n'en avait que plusieurs non vérifiées exceptions (Initialization Exception, ConfigurationException, etc.) et je dois dire que ce n'était pas très sympathique . La plupart du temps, vous avez dû intercepter ou relancer des exceptions que vous ne savez pas comment gérer, ou vous ne vous en souciez même pas (à ne pas confondre avec vous, vous devez ignorer les exceptions), en particulier du côté client, où un seul le clic pourrait déclencher 10 exceptions possibles (vérifiées).

La version actuelle (3e) n'utilise que des exceptions non vérifiées, et elle dispose d'un gestionnaire d'exceptions global qui est responsable de gérer tout ce qui n'est pas intercepté. L'API fournit un moyen d'enregistrer les gestionnaires d'exceptions, qui décideront si une exception est considérée comme une erreur (la plupart du temps c'est le cas), ce qui signifie enregistrer et notifier quelqu'un, ou cela peut signifier autre chose - comme cette exception, AbortException, ce qui signifie casser le thread d'exécution actuel et ne pas enregistrer d'erreur car il est souhaitable de ne pas le faire. Bien sûr, pour travailler, tous les threads personnalisés doivent gérer la méthode run () avec un try {...} catch (all).

public void run () {

try {
     ... do something ...
} catch (Throwable throwable) {
     ApplicationContext.getExceptionService().handleException("Handle this exception", throwable);
}

}

Cela n'est pas nécessaire si vous utilisez WorkerService pour planifier des travaux (Runnable, Callable, Worker), qui gère tout pour vous.

Bien sûr, ce n'est que mon opinion, et ce n'est peut-être pas la bonne, mais cela me semble être une bonne approche. Je verrai après que je publierai le projet si ce que je pense est bon pour moi, c'est bon pour les autres aussi ... :)

adrian.tarau
la source