La gestion des exceptions en java

En auditant des applications pour des clients d’OCTO, je me suis aperçu que la gestion des exceptions est un élément qui fait souvent défaut au même titre que la gestion des transactions.
Ce billet était à l’origine des notes personnelles qui avaient pour but de me servir de piqure de rappel et je me suis dit qu’un article de blog serait peut être utile à tous.
Ce sujet prête souvent à discussions et il faut parfois adapter au cas par cas, néanmoins avoir un cadre de bonnes pratiques peut s’avérer très utile.

Tout d’abord, voici un schéma présentant la hiérarchie des classes « mères » d’exceptions en java.

On constate donc 3 degrés de gravité :

  • Les erreurs graves qui causent généralement l’arrêt du programme et qui sont représentées par la classe java.lang.Error
    Ces erreurs empêchent le bon fonctionnement de la JVM (exemple : OutOfMemoryError). 99,9% du temps on ne doit pas les rattraper.
    On peut les attraper mais c’est un anti pattern car on ne peut globalement rien faire pour les arranger, les attraper risque donc d’empêcher un problème très important de remonter. Vous ne devriez pas avoir à utiliser cette classe à moins de développer un framework extrêmement technique.
  • Les erreurs qui doivent être traitées par l’appelant de la méthode qui lève l’exception. Elles héritent de la classe java.lang.Exception ce sont elles qu’on appelle exceptions Checked. Elles doivent être déclarées dans la signature de la méthode après le mot clé throws, ces exceptions sont donc explicites.
    Il s’agit d’erreur au niveau applicatif qui vont se propager dans le code en ignorant les instructions suivantes.
  • Les erreurs qui peuvent ne pas être traitées et qui sont des objets de la classe java.lang.RuntimeException. Ce sont elles qu’on appelle exceptions Unchecked. Ce type d’exceptions peut être levé même sans déclaration préalable dans le throws. Ces exceptions sont donc implicites.
    Ex : ArrayIndexOutOfBoundsExceptions


Attention cependant, l’API de base ne respecte pas tout à fait ce modèle.
Dans Java il y a plusieurs branches de checked et unchecked Exception : les classes nommées XxxException ne sont pas toutes des Exceptions !
Par exemple : com.sun.corba.se.impl.io.TypeMismatchException n’est pas une exception mais une erreur. Certaines erreurs n’ont pas le suffixe Error (java.lang.ThreadDeath ou com.sun.tools.javac.jvm.ClassWriter$StringOverflow), de même sun.tools.java.AmbiguousMember est une exception, il faudra donc être particulièrement attentif à la hiérarchie.

On va alors pouvoir décliner ces exceptions en plusieurs types :

  • Les exceptions applicatives (fonctionnelles) : par exemple SoldeInsuffisantException
  • Les exceptions techniques ou explicites Java : par exemple SQLException ou IOException
  • Enfin, les exceptions technique de la JVM : NullPointerExceptions, ArrayIndexOutOfBoundsException…

Globalement une bonne pratique à appliquer est de n’attraper que les exceptions que l’on sait traiter (dans 95% des cas ‘tracer une exception dans un fichier de log’ n’est pas ‘traiter une exception). Si l’on ne sait pas quoi faire d’une exception, il vaut mieux la laisser remonter. En dernier lieu une couche de framework devra les rattraper et afficher une page d’erreur à l’utilisateur avec un message sympathique et non une stacktrace.

De plus, les exceptions requièrent du design (une hiérarchie d’exceptions) afin de vous permettre de les lancer/attraper finement et d’avoir une remontée informative des erreurs. De plus Java 7 permettra de rattraper plusieurs exceptions en une seule déclaration.
L’exemple suivant montre un anti-pattern de gestion des exceptions classique qui va poser plusieurs problèmes.
D’une part l’utilisateur ne sait pas qu’il s’est produit un problème, d’une autre les développeurs et l’exploitation ne savent pas quel problème s’est produit précisément car on n’inscrit rien d’exploitable dans les logs.

try {
    Connection con = connectionManager.getConnection();
    Statement st = con.createStatement("select * from maTable");
    ResultSet rs = st.executeQuery();
    MyFile f = new File("monfichier");
    f.writeResultSet(rs)
} catch (TechnicalException ex) {
    logger.error("TechnicalException !!! ");
} catch (FunctionnalException ex2) {
    logger.error("FunctionnalException !!!");
}

Il vaudrait mieux envisager un traitement de ce type :

try {
    Connection con = connectionManager.getConnection();
    Statement st = con.createStatement("select * from maTable");
    ResultSet rs = st.executeQuery();
    MyFile f = new File("monfichier");
    f.writeResultSet(rs);
} catch (SQLException sqlex) {
    throw new WorkflowStepException(ioex);
} catch (IOException ioex) {
    throw new WorkflowStepException(ioex);
} catch (WorkflowStepException wkflowex) {
    rollbackWorkflowForStep(Step.GET_TABLE_AND_WRITE, wkflowex);
} catch (FileNotFoundException ex) {
    processCreateFichierNotExisting();
} catch (SQLTimeoutException) {
    sendMailDatabaseIsDown();
}

Bonnes pratiques :

Voici un listing de quelques bonnes pratiques assez communes sur la gestion des exceptions en Java. Bien entendu ce sujet amène toujours à discussion et il ne s’agit pas là d’un modèle universel à appliquer dans tous les contextes mais plutôt de patterns à retenir.

Ne jamais ignorer une exception …

.. par exemple par un catch vide contenant juste un log ou affichage de la stacktrace.

L’exemple à ne pas faire (et pourtant si fréquent dans nos applications):

try {
    maMethodeQuiPlante();
} catch (Exception ex) {
    logger.debug("Exception :" +  ex.getMessage());
    ex.printStackTrace();
}
//Non

Il vaudrait mieux envisager quelque chose comme cela :

try {
    maMethodeQuiPlante();
} catch (MaMethodException ex) {
    processProblem(ex);
}
//Oui

Dans le cas ou l’on ne sait pas quoi faire dans le bloc catch : laisser remonter l’exception à un composant technique qui saura en faire quelque chose, ou au pire affichera une page d’erreur à l’utilisateur.

Utiliser throws de manière exhaustive

Si on a Exception A héritée par B et C on met throws A,B,C et pas throws A afin de savoir ce qui s’est vraiment passé.

class AException extends Exception;
class BException extends AException;
class CException extends AException;

void maMethod() throws A // Non
void maMethod() throws A,B,C // Oui

Les exceptions ne sont pas faites pour le contrôle de flux

Ce mode de fonctionnement n’est pas efficace, difficile à relire et à modifier.

Exemple :

//monattribut est null
try {
    monattribut.maMethod();
} catch (NullPointerException npe) {
    context.reAskAttribute();
}

Il vaudrait mieux faire :

if (monattribut != null) {
    monattribut.maMethod();
} else {
    context.reAskAttribute();
}

De plus, une NullPointerException ne devrait pas être attrapée : il s’agit toujours d’un bug à corriger dans son code.

Pattern d’entrées/sorties

Pour les entrées sorties utiliser le pattern suivant. Le code en soit n’a bien sûr pas d’intérêt : il ne fait que présenter la construction du try/catch/finally)

try{
    //declaration de la ressource
    File file = new File("monfichier.txt")
    try{
        //utilisation de la ressource
        file.write("un truc");
    } finally {
        //fermeture de la ressource
        file.close();
    }
} catch(IOException ex) {
    //traitement de l'exception
    traitementException(ex);
}

Pas de return dans un bloc finally

Le return permet de quitter la méthode en cours d’exécution. Mais si un finally existe, ce dernier sera tout de même exécuté. Le bloc finally doit uniquement servir à maintenir l’intégrité et notamment libérer des ressources utilisées par le bloc try (cf. exemple précédent).
Exemple :

public String retourDansUnFinally(){
    try{
        throw new RuntimeException("Je veux planter");
    } finally {
        return "Non !";
    }
}

@Test
public void testRetourDansUnFinally(){
    assertEquals("Non !", retourDansUnFinally());
}

Ce test passera au vert. Dans ce cas l’erreur saute aux yeux, mais si l’exception est levée par une méthode dont la taille dépasse celle de l’écran on ne verra plus rien.

Utiliser les exceptions standards

Evitez de réinventer la roue.
Tout le monde les connait, votre code sera donc plus facilement lisible. De plus elles sont documentées (javadoc) et sont adaptées aux cas prévus (FileNotFoundException, SQLException, NullPointerException, TimeoutException …)

Une exception peut en cacher une autre

En rattrapant une exception on peut décider d’en lancer une autre. Il faut toutefois utiliser le constructeur d’exception qui peut prendre une exception en paramètre afin d’éviter de perdre les informations associées à la première. Il est aussi conseiller de lui adjoindre un message explicite.

throw new CustomException("message explicite", 
        otherException);

Ne pas utiliser de codes de retour dans des exceptions

Il faut faire le deuil des codes retours, les exceptions ont été conçues pour fiabiliser ce modèle. Par exemple SQLException qui oblige à l’attraper pour analyser son contenu…

try {
...
} catch (SQLException e) {
    if (e.getErrorCode() == 42) {
        responseToUniverse();
    } else {
        throw e;
    }
}

Ne pas déclarer/attraper des erreurs plus large que celle qui peuvent survenir

– Une méthode lance A ou B qui héritent de C, on mettra throws A, B et pas throws C car si une nouvelle exception hérite de C on sera susceptible de l’attraper sans savoir la traiter.
– Un autre cas peut s’illustrer par l’exemple suivant :

Imaginons une interface SimpleInterface déclarant une méthode ‘public void work()’
Cette interface a de nombreuses implémentations dont certaines inconnues (développées par d’autres équipes par exemple), mais mon code (Boss) peut exécuter toutes ces implémentations.
Le code la classe Boss pourrait ressembler à cela :

public void manage(Team team) {
    for (IEmployee teamMember : team.getMembers()) {
        try {
            teamMember.work();
            reportGoodMember(teamMember);
        } catch (Exception e) {
            rememberThisGuyForHisEndOfYearNegociation(teamMember);
        }
    }
}

Nous avons vu qu’il n’y a aucune raison d’attraper si large – ici : toutes les exceptions. En effet, le compilateur nous garantit qu’on ne peut pas avoir autre chose qu’une RuntimeException ou une Error car l’interface de la méthode work ne déclare aucune exception. Le code suivant est donc équivalent et serait suffisant :

public void manage(Team team) {
    for (IEmployee teamMember : team.getMembers()) {
        try {
            teamMember.work();
            reportGoodMember(teamMember);
        } catch (RuntimeException e) {
            rememberThisGuyForHisEndOfYearNegociation(teamMember);
        }
    }
}

Cependant, puisque le code fait la même chose, on peut penser que ça ne pose pas de problèmes. C’est vrai, mais cela peut très vite devenir problématique si l’on décide finalement de propager l’erreur.

public void manage(Team team) throws Exception {
    for (IEmployee teamMember : team.getMembers()) {
        try {
            teamMember.work();
            reportGoodMember(teamMember);
        } catch (Exception e) {
            if (e instanceof LeReveilNAPasSonneException) {
                rememberThisGuyForHisEndOfYearNegociation(teamMember);
            } else {
                throw e;
            }
        }
    }
}

Dans ce cas, la méthode manage va devoir déclarer des exceptions qu’elle ne lancera jamais (le compilateur le garantit). Et le code appelant la méthode manage va devoir gérer des exceptions qui ne se produiront jamais. C’est un peu subtil mais très important à comprendre.
En effet, étant donné que la méthode manage déclare maintenant un throws Exception dans sa signature, elle déclare qu’elle peut lancer tout type d’exception : on doit entourer ses appels d’un try / catch correspondant au scope du throws (ici exception).
Donc bien qu’on ai tenté de traiter une exception dont on sait quoi faire (bonne pratique), on force le code appelant à attraper toutes les exceptions qui étendent Exception (mauvaise pratique). Or il n’y a aucune de chance que le code de la méthode manage puisse toutes les lancer …

La solution à ce problème est donc :

public void manage(Team team)  {
    for (IEmployee teamMember : team.getMembers()) {
        try {
            teamMember.work();
            reportGoodMember(teamMember);
        } catch (LeReveilNAPasSonneException e) {
                rememberThisGuyForHisEndOfYearNegociation(teamMember);
        }
    }
}

Si une autre exception remonte, on ne sait pas la traiter, donc on l’ignore (on la laisse se propager) et une couche technique supérieure devra la gérer.

Bien entendu cet article n’est pas exhaustif, mais nous espérons que ces quelques bonnes pratiques vous aideront à implémenter une gestion des erreurs appropriée et suffisamment informative.

13 commentaires sur “La gestion des exceptions en java”

  • Excellent article ! Ca fait jamais de mal de rappeler certaines choses, surtout quand on voit ce qu'on voit dans les audits...
  • Bon article, qu'il faudrait diffuser dans certaines directions technique ou ce genre de pratique n'est pas encore très évidentes...
  • Deux remarques : - il faut faire attention à l'inflation du nombre d'exception levée. Si les exceptions sont identifiées sur chaque méthodes cela peut devenir très gênant lors de l'utilisation, des changements de version, des montées de version "Ne pas utiliser de codes de retour dans des exceptions" avoir des codes erreurs permet aussi bien aux fonctionnels qu'aux exploitants de parler de la même chose. - la gestion des exceptions avec l'AOP est largement à recommender pour toutes les exceptions techniques à minima.
  • Moi qui trouvait que PHP était vraiment horrible dans sa gestion des exceptions... Bon ok, je trouve toujours que c'est horrible, mais quand je lis que com.sun.corba.se.impl.io.TypeMismatchException n'est pas une Exception, je suis rassurée. Ou pas.
  • @Gregoire Pouvez-vous m'éclairer sur vos remarques : * "Si les exceptions sont identifiées sur chaque méthodes cela peut devenir très gênant lors de l’utilisation, des changements de version, des montées de version" Parlez vous de déclarer les exceptions (lorsque vous parlez "d'identifier") ou s'agit-il d'autre chose ? Il faut noter que les exceptions font partie de l'API d'une méthode. En Java les exceptions font partiellement parties de la signature d'une méthode (il n'est pas possible pour une implémentation de déclarer des exceptions (checked) plus large que ce qui est déclaré dans l'interface). * "« Ne pas utiliser de codes de retour dans des exceptions » avoir des codes erreurs permet aussi bien aux fonctionnels qu’aux exploitants de parler de la même chose." Si les codes font partie du langage des fonctionnels pourquoi pas, mais personnellement j'ai le sentiment qu'une exception nommée ForeignKeyViolationParentKeyNotFoundException (on peut discuter du nom de la classe :-) ) est nettement plus clair que le code erreur ORA-02291. Le nom complet d'une classe est déjà un code unique pour une exception. Et surtout les exceptions ont des propriétés que n'ont pas les codes d'erreur : ** Un nom explicite ** une hiérarchie : toutes les exceptions concernant une clés étrangères pourraient étendre ForeignKeyViolationException ** N'obligent pas à attraper toutes les exceptions pour regarder le code d'erreur interne (cf l'exemple "Ne pas utiliser de codes de retour dans des exceptions") * "la gestion des exceptions avec l’AOP est largement à recommender pour toutes les exceptions techniques à minima." A quoi pensez-vous ? Pour moi la gestion des exceptions est fait sur mesure en fonction du contexte et non pas de façon systématique (avec des Aspects). On pourrait imaginer vouloir loguer par exemple toutes les exceptions lancées avec de l'AOP mais cela reviendrait à tracer des Exceptions qui ne sont pas forcément des erreurs. En effet dans certains cas, un traitement va lever une exceptions, mais l'appelant connait peut-être une solution de secours. Par exemple : écrire dans un fichier si le SI d'un partenaire n'est pas disponible. Je me souviens avoir voulu faire du zèle lors d'un de mes premiers framework Java : toutes les exceptions traçaient leurs messages dans les logs lors de leur instanciation (en partant du principe, que si on les instanciaient, c'était pour les lancer). Cela n'a pas résisté aux premières implémentations fonctionnelles, pour les mêmes raisons. En l'occurrence, le premier cas qui nous a été remonté était technique : un format invalide. Il se trouve que selon l'origine des messages certains champs pouvaient être dans 2 formats. Il n'était pas question de tracer un message d'erreur tant que tous les formats acceptés n'avaient pas été essayé...
  • com.sun.* sont internes à la JVM, on est pas sensé les connaitre.
  • Une (bonne???!) pratique qui se répend depuis qq tps déjà est l'utilisation de runtime exceptions au lieu de checked exceptions. L'idée est de ne catcher que ce que l'on veut contrôler, et laisser le reste remonter... Ceci demande de la rigueur (lire la doc :-) pour gérer les incertitudes sur les exceptions à lever p.ex.), mais à l'avantage de simplifier la compréhension/gestion du code en pratique. Sans doute une stratégie dangereuse pour les moins expérimentés. Tout un débat... (cf. Des threads entiers sur le forum de Spring ont débatu ce sujet depuis qq années.) FYI, en général les puristes du langage n'apprécient pas cette approche vue comme un contournement de la fcté d'origine.
  • >Je me souviens avoir voulu faire du zèle lors d’un de mes premiers framework Java : > toutes les exceptions traçaient leurs messages dans les logs lors de leur instanciation > (en partant du principe, que si on les instanciaient, c’était pour les lancer). Marrant, j;ai fait la meme, sauf que moi je mettais aussi la stack trace, bonjour les logs. remarque une fois, j'ai vu la meme avec un petit bonus : ils avaient mis des exceptions par couche "je catch et je renvoi", 500 lignes pour une exception de format, c'etait sympa a regarder.
  • @JC, +1 pour les exceptions runtime... mais pas dans tousles cas. En fait, perso j'adopte le : technique => runtime (ex: MachinInaccessibleException) fonctionnel => checked (ex: UtilisationArmeImpossibleNiveauInsuffisantException)
  • Il y a, selon moi, un cas concret où il est "bon" de déclarer un "throws Throwable" (ou "throws Exception") au niveau de sa méthode, sans avoir à réfléchir : lors de l'écriture d'un test unitaire ! En effet, combien de fois vois-t-on une méthode du type : public void testTruc(){ try { unAppelQuiEstSusceptibleDeLancerUneException(); } catch(MonException e){ fail(e.getMessage()); } } Ce (mauvais) exemple a trois inconvénients : - Si une exception survient, le test reportera une erreur sur la ligne du catch .. - Si une exception survient, on perd la stacktrace - Il est peu lisible Alors qu'avoir fait le code suivant : public void testTruc() throws Throwable { unAppelQuiEstSusceptibleDeLancerUneException(); } Ne présente aucun des 3 inconvénients cités précédemment :)
  • Bonsoir, @Philippe Kernevez - oui identifié == déclaré, Entièrement d'accord avec ton propos, qlques bemols ; cela fonctionne bien dans une API technique de type DOM, JAX-RS et autre JSR*** etc... deplus l'implementation est souvent encapsulé dans un framework maison pour plus d'abstraction le tout embarqué dans une application. Malheureusement dans les applications il y a aussi du fonctionnel et la il faut trouver un nom à chacune des exceptions sans parler de la convention (FrenchException, GlobishException). Cela fonctionne avec 10 exceptions avec 100 voir 200 exceptions pour une application ce n'est plus possible. Il y a plus d'exception déclaré que de paramétres passé à la méthode. Le pire c'est quand le service ou l'application est exposé en webservice, les clients doivent régénérer à chaque fois un nouveau stub dès qu'une nouvelle exception est déclaré dans une sous couche, a moins d'utiliser le DP Facade. - Ok pour l'utilisation du nom pour les exceptions techniques, mais bon c'est un choix de RT. contre argument HTTP utilise les codes erreurs depuis tjrs et ca ne gene personne 200 OK, 404 NOT FOUND, 403 Forbidden. Le pb de l'erreur ORA-02291 ce n'est pas sont formalise en code qui pose pb, c'est plutôt l'acces à la documentation qui est difficile. Pour la hiérarchie des exceptions ca ne fonctionne plus lorsqu'il y a 100 ou 200 erreurs à gérer et encore moins avec plusieurs applications interconnectées. SNMP fonctionne très bien avec des OIDs (arbres d'OIDS etc...). L'utilisation de code n'est pas trop dur à comprendre pour l'ensemble des acteurs fonctionnels (java, exception, throwable, unckecked il s'en fout royalement), développeur (n'ont pas besoins de réfléchir à trouver des noms bizarres à chaque fois), exploitants (préfèrent des codes à la sauce SNMP avec une jolie doc associée). Deplus les fichiers de logs sont mieux formater en utilisant les codes et ressorte plus facilement lors de la lecture. Les exceptions sont très orientés POO et fonctionnent très bien dans une application "standalone/ihm" (Eclipse par ex), mais lorsque l'on dialogue avec l’extérieur c'est plus dur à gerer. Deux approches sont possibles à la sauce API Flick qui utilise des code erreurs ou bien Amazon API qui utilise des noms pour les erreurs mappés sur code HTTP. - Il faut simplement voir la granularité des exceptions et de la regle metier/technique à developper. De toute facon pour moi la creation, le log et le lancement de l'exception est tjrs delegué à une ExceptionFactory qui se charge de faire tout le travail, j'interdis toute création et lancement d'exception explicite par les développeurs, c'est purement impossible de refactorer le code sinon. L'utilisation de l'AOP peut etre utile lorsqu'il faut masquer, encapsuler des exceptions ou les traiter globalement; par exemple une exception DataAccessException peut etre encapsuler dans TechniqueException avant d’être lancer au service ou bien dans le cas des NPE l’intérêt est aussi d'encapsuler l'exception mais de logger la stacktrace associée. @Benoit Lafontaine J'utilise deux types d'exceptions. TechniqueException heritant RuntimeException juste pour l'architecte et le RT dans le cas des frameworks technique FonctionnelleException heritant d'Exception pour le developpeur uniquement.
  • @Gregoire Il y a un point important dans mon approche : les exceptions concerne le cœur du logiciel, pas ses frontières. Les exceptions permettent de remonter des informations précises au sein du logiciel (soit pour identifier un bug soit permettre à un appelant de prendre une décision) et n'ont pas de raison d'être exposé à l'extérieur du logiciel par webservice. Des codes d'erreurs pour des protocoles de communication sont plus adaptés, entre autre pour ne pas avoir à sérialiser des objets inconnus (une nouvelle exception runtime non anticipée par le générateur). Les humains font partie des frontières : le résultat d'un batch destiné à être lu en cas de souci par un opérateur devra sortir un code d'erreur (documenté) et non une exception. D'ailleurs c'est comme ça que fonctionne les batch sur le MainFrame et dans JBossBatch. Par contre le log contiendra des informations sur les exceptions qui se sont produites pour retourner un code d'erreur. "développeur (n’ont pas besoins de réfléchir à trouver des noms bizarres à chaque fois)" : cela s'applique aussi pour le reste de l'application. Une recette que j'essaye de m'appliquer : 'si je n'arrive pas à trouver un nom explicite, c'est que j'ai probablement une erreur de design'. @Frédéric 100% d'accord avec cette exception que je pratique moi-même
  • Merci pour cet article fort intéressant. Je ne trouve également pas très fair-play de prendre comme exemple des classes que l'on est censé ignorer (com.sun....). Il y a sûrement des mauvais exemples dans l'API publiques du JDK :-) Je ne connaissais pas le pattern avec le finally à l'intérieur du try/catch : ça créé une imbrication supplémentaire, mais c'est ingénieux. @JC et @Benoît Lafontaine Le fait de remplacer les checked exceptions par des runtimes exception est un sujet de controverses apparaissant souvent ces derniers temps (sur les blogs techniques) et est à mon avis apparu à la suite des frameworks comme Spring qui transforment les checked exception en runtime par AOP. Dire que c'est une bonne pratique est un raccourcis trop rapide (et à mon sens largement faux). L'origine de la controverse (fin 2009) est due à une discussion de Bruce Eckel avec d'autres personnes qui considèrent que : - les développeurs gèrent très mal les checked Exceptions, alors il vaut mieux s'en passer ; - dans les autres langages main-stream, il n'y a pas ces notions de checked exceptions. La position de Sun (Snorcle) sur le sujet est claire : http://download.oracle.com/javase/tutorial/essential/exceptions/runtime.html Le billet suivant : http://ralkie.blogspot.com/2009/10/java-exceptions-checked-vs-unchecked.html est un récapitulatif sur cette controverse et la position des acteurs majeurs du monde Java (Josh Bloch, James Gosling, Bruce Eckel, Bill Venners...) : la "bonne pratique" ne fait pas l'unanimité. Et le billet suivant : http://www.mindview.net/Etc/Discussions/CheckedExceptions analyse que les deux positions sont semblables aux positions 'langages dynamiques'/'langages statiquement typés'. Les checked exceptions nous obligent à mettre plus de code qui peut sembler (boilerplate), mais nous obligent à la compilation à prendre en compte des conditions exceptionnelles. En ce qui me concerne, si je fais du Java (et d'autres choses de statiquement typé), je choisis de considérer les checked exceptions quand c'est pertinent.
    1. Laisser un commentaire

      Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *


      Ce formulaire est protégé par Google Recaptcha