Développons en Java v 2.40 Copyright (C) 1999-2023 Jean-Michel DOUDOUX. |
|||||||
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Niveau : | ![]() |
Les exceptions représentent le mécanisme de gestion des erreurs intégré au langage Java. Il se compose d'objets représentant les erreurs et d'un ensemble de trois mots clés qui permettent de détecter et de traiter ces erreurs (try, catch et finally ) mais aussi de les lever ou les propager (throw et throws).
Lors de la détection d'une erreur, un objet qui hérite de la classe Exception est créé (on dit qu'une exception est levée) et propagé à travers la pile d'exécution jusqu'à ce qu'il soit traité.
Ces mécanismes permettent de renforcer la sécurité du code Java.
Exemple : une exception levée à l'exécution non capturée |
Résultat : |
Si dans un bloc de code on fait appel à une méthode qui peut potentiellement générer une exception, on doit soit essayer de la récupérer avec try/catch, soit ajouter le mot clé throws dans la déclaration du bloc. Si on ne le fait pas, il y a une erreur à la compilation. Les erreurs et exceptions du paquetage java.lang échappent à cette contrainte. Throws permet de déléguer la responsabilité des erreurs à la méthode appelante
Ce procédé présente un inconvénient : de nombreuses méthodes des packages java indiquent dans leur déclaration qu'elles peuvent lever une exception. Cependant ceci garantit que certaines exceptions critiques seront prises explicitement en compte par le programmeur.
Ce chapitre contient plusieurs sections :
Le bloc try rassemble les appels de méthodes susceptibles de produire des erreurs ou des exceptions. L'instruction try est suivie d'instructions entre des accolades.
Exemple ( code Java 1.1 ) : |
Si un événement indésirable survient dans le bloc try, la partie éventuellement non exécutée de ce bloc est abandonnée et le premier bloc catch est traité. Si un bloc catch est défini pour capturer l'exception issue du bloc try alors elle est traitée en exécutant le code associé au bloc. Si le bloc catch est vide (aucune instruction entre les accolades) alors l'exception capturée est ignorée. Une telle utilisation de l'instruction try/catch n'est pas une bonne pratique : il est préférable de toujours apporter un traitement adapté lors de la capture d'une exception.
S'il y a plusieurs types d'erreurs et d'exceptions à intercepter, il faut définir autant de blocs catch que de types d'événements. Par type d'exception, il faut comprendre « qui est du type de la classe de l'exception ou d'une de ses sous-classes ». Ainsi dans l'ordre séquentiel des clauses catch, un type d'exception ne doit pas venir après un type d'une exception d'une super-classe. Il faut faire attention à l'ordre des clauses catch pour traiter en premier les exceptions les plus précises (sous-classes) avant les exceptions plus générales. Un message d'erreur est émis par le compilateur dans le cas contraire.
Exemple ( code Java 1.1 ) : erreur à la compil car Exception est traité en premier alors que ArithmeticException est une sous-classe de Exception |
Résultat : |
Si l'exception générée est une instance de la classe déclarée dans la clause catch ou d'une classe dérivée, alors on exécute le bloc associé. Si l'exception n'est pas traitée par un bloc catch, elle sera transmise au bloc de niveau supérieur. Si l'on ne se trouve pas dans un autre bloc try, on quitte la méthode en cours, qui regénère à son tour une exception dans la méthode appelante.
L'exécution totale du bloc try et d'un bloc d'une clause catch sont mutuellement exclusives : si une exception est levée, l'exécution du bloc try est arrêtée et si elle existe, la clause catch adéquate est exécutée.
La clause finally définit un bloc qui sera toujours exécuté, qu'une exception soit levée ou non. Ce bloc est facultatif. Il est aussi exécuté si dans le bloc try il y a une instruction break ou continue.
Cette classe descend directement de la classe Object : c'est la classe de base pour le traitement des erreurs.
Cette classe possède deux constructeurs :
Méthode | Rôle |
Throwable() | |
Throwable(String) | La chaîne en paramètre permet de définir un message qui décrit l'exception et qui pourra être consulté dans un bloc catch. |
Les principales méthodes de la classe Throwable sont :
Méthodes | Rôle |
String getMessage( ) | lecture du message |
void printStackTrace( ) | affiche l'exception et l'état de la pile d'exécution au moment de son appel |
void printStackTrace(PrintStream s) | Idem mais envoie le résultat dans un flux |
Exemple ( code Java 1.1 ) : |
Résultat : |
Ces trois classes descendent de Throwable : en fait, toutes les exceptions dérivent de la classe Throwable.
La classe Error représente une erreur grave intervenue dans la machine virtuelle Java ou dans un sous système Java. L'application Java s'arrête instantanément dès l'apparition d'une exception de la classe Error.
La classe Exception représente des erreurs moins graves. Les exceptions héritant de la classe RuntimeException n'ont pas besoin d'être détectées impérativement par des blocs try/catch.
Pour générer une exception, il suffit d'utiliser le mot clé throw, suivi d'un objet dont la classe dérive de Throwable. Si l'on veut générer une exception dans une méthode avec throw, il faut l'indiquer dans la déclaration de la méthode, en utilisant le mot clé throws.
En cas de nécessité, on peut créer ses propres exceptions. Elles descendent des classes Exception ou RunTimeException mais pas de la classe Error. Il est préférable (par convention) d'inclure le mot « Exception » dans le nom de la nouvelle classe.
Exemple ( code Java 1.1 ) : |
Les méthodes pouvant lever des exceptions doivent inclure une clause throws nom_exception dans leur en-tête. L'objectif est double : avoir une valeur documentaire et préciser au compilateur que cette méthode pourra lever cette exception et que toute méthode qui l'appelle devra prendre en compte cette exception (traitement ou propagation).
Si la méthode appelante ne traite pas l'erreur ou ne la propage pas, le compilateur génère l'exception nom_exception must be caught or it must be declared in the throws clause of this method.
Java n'oblige à déclarer les exceptions dans l'en-tête de la méthode que pour les exceptions dites contrôlées (checked). Les exceptions non contrôlées (unchecked) peuvent être capturées mais n'ont pas à être déclarées. Les exceptions et erreurs qui héritent de RunTimeException et de Error sont non contrôlées. Toutes les autres exceptions sont contrôlées.
Il est fréquent durant le traitement d'une exception de lever une autre exception. Pour ne pas perdre la trace de l'exception d'origine, Java propose le chaînage d'exceptions pour conserver l'empilement des exceptions levées durant les traitements.
Il y a deux façons de chaîner deux exceptions :
Exemple : |
Résultat : |
La méthode getCause() héritée de Throwable permet d'obtenir l'exception originale.
Exemple : |
Résultat : |
Il est préférable d'utiliser les exceptions fournies par Java lorsqu'une de ces exceptions répond au besoin plutôt que de définir sa propre exception.
Il existe trois types d'exceptions :
Les exceptions de type Error et RuntimeException sont dites unchecked exceptions car les méthodes n'ont pas d'obligation à les traiter ou à déclarer leur propagation explicitement. Ceci se justifie par le fait que leur levée n'est pas facilement prédictible.
Il n'est pas recommandé de créer ses propres exceptions en dérivant d'une exception de type unchecked (classe de type RuntimeException). Même si cela peut sembler plus facile puisqu'il n'est pas obligatoire de déclarer leur propagation, cela peut engendrer certaines difficultés, notamment :
Cependant, l'utilisation d'exceptions de type unchecked se répend de plus en plus notamment depuis la diffusion de la plate-forme .Net qui ne propose que ce type d'exceptions.
Des ressources comme des fichiers, des flux, des connexions, ... doivent être fermées explicitement par le développeur pour libérer les ressources sous-jacentes qu'elles utilisent. Généralement cela est fait en utilisant un bloc try / finally pour garantir leur fermeture dans la quasi-totalité des cas.
De plus, la nécessité de fermer explicitement la ressource implique un risque potentiel d'oubli de fermeture qui entraine généralement une fuite de ressources.
Avec Java 7, l'instruction try avec ressource permet de définir une ressource qui sera automatiquement fermée à la fin de l'exécution du bloc de code de l'instruction.
Ce mécanisme est aussi désigné par l'acronyme ARM (Automatic Ressource Management).
Avant Java 7, il était nécessaire d'utiliser un bloc finally pour s'assurer que le flux sera fermé même si une exception est levée durant les traitements. Ce type de traitement possède plusieurs inconvénients :
Exemple : |
L'inconvénient de cette solution est que l'exception propagée serait celle de la méthode close() si elle lève une exception qui pourrait alors masquer une exception levée dans le bloc try. Il est possible de capturer l'exception de la méthode close().
Exemple : |
L'inconvénient de cette solution est que l'exception qui peut être levée par la méthode close() n'est pas propagée. De plus la quantité de code produite devient plus importante.
Avec Java 7, le mot clé try peut être utilisé pour déclarer une ou plusieurs ressources.
Une ressource est un objet qui doit être fermé lorsque l'on a plus besoin de lui : généralement cette ressource encapsule ou utilise des ressources du système : fichiers, flux, connexions vers des serveurs, ...
Une nouvelle interface a été définie pour indiquer qu'une ressource peut être fermée automatiquement : java.lang.AutoCloseable.
Tous les objets qui implémentent l'interface java.lang.AutoCloseable peuvent être utilisés dans une instruction de type try-with-resources. L'instruction try avec des ressources garantit que chaque ressource déclarée sera fermée à la fin de l'exécution de son bloc de traitement.
L'interface java.lang.Autocloseable possède une unique méthode close() qui sera invoquée pour fermer automatiquement la ressource encapsulée par l'implémentation de l'interface.
L'interface java.io.Closable introduite par Java 5 hérite de l'interface AutoCloseable : ainsi toutes les classes qui implémentent l'interface Closable peuvent être utilisées comme ressource dans une instruction try-with-resource.
La méthode close() de l'interface Closeable lève une exception de type IOException alors que la méthode close() de l'interface AutoCloseable lève une exception de type Exception. Cela permet aux interfaces filles de AutoCloseable de redéfinir la méthode close() pour qu'elles puissent lever une exception plus spécifique ou aucune exception.
Contrairement à la méthode close() de l'interface Closeable, une implémentation de la méthode close() de l'interface AutoCloseable n'est pas supposée être idempotente : son invocation une seconde fois peut avoir des effets de bords.
Une implémentation de la méthode close() de l'interface AutoCloseable() devrait déclarer une exception plus précise que simplement Exception ou ne pas déclarer d'exception du tout si l'opération de fermeture ne peut échouer.
Il faut garder à l'esprit que l'exception levée sera masquée par l'instruction try-with-resource : l'implémentation de la méthode close() doit faire attention aux exceptions qu'elle peut lever (par exemple, comme le précise la Javadoc, elle ne doit pas lever une exception de type InterruptedException)
L'instruction try avec des ressources utilise le mot clé try avec une ou plusieurs ressources définies dans sa portée, chacune séparée par un point-virgule.
Exemple ( code Java 7 ) : |
Dans l'exemple ci-dessus, la ressource de type BufferedReader sera fermée proprement à la fin normale ou anormale des traitements.
Les ressources sont implicitement final : il n'est donc pas possible de leur affecter une nouvelle instance dans le bloc de l'instruction try.
Une instruction try avec ressources peut avoir des clauses catch et finally comme une instruction try classique. Avec l'instruction try avec ressources, les clauses catch et finally sont exécutées après que la ou les ressources ont été fermées.
Exemple ( code Java 7 ) : |
Il est possible de déclarer plusieurs ressources dans une même instruction try avec ressources, chacune séparée par un caractère point-virgule. Dans ce cas, la méthode close() des ressources déclarées est invoquée dans l'ordre inverse de leur déclaration.
L'instruction try-with-resource présente un petit inconvénient : il est obligatoire de définir la variable qui encapsule la ressource entre les parenthèses qui suivent l'instruction try. Il n'est par exemple pas possible de fournir en paramètre de l'instruction try une instance déjà créé.
Exemple ( code Java 7 ) : |
Le compilateur génère une erreur lors de la compilation de ce code.
Résultat : |
L'exemple ci-dessus génère une erreur à la compilation puisqu'aucune variable n'est définie entre les parenthèses de l'instruction try.
Pour pallier ce petit inconvénient, il est possible de définir une variable et de l'initialiser avec l'instance existante.
Exemple ( code Java 7 ) : |
Dans l'exemple ci-dessus, comme la variable définie et celle existante pointent sur la même référence, les deux variables peuvent être utilisées indifféremment. L'instruction try-with-resource se charge de fermer automatiquement le flux.
Attention, seules les ressources déclarées dans l'instruction try seront fermées automatiquement. Si une ressource est explicitement instanciée dans le bloc try, la gestion de la fermeture et de l'exception qu'elle peut lever doit être gérée par le développeur.
Une exception peut être levée dans le bloc de l'instruction try mais aussi durant l'invocation de la méthode close() de la ou des ressources déclarées. La méthode close() pouvant lever une exception, celle-ci pourrait masquer une éventuelle exception levée dans le bloc de code de l'instruction try.
Il est obligatoire de gérer l'exception pouvant être levée par la méthode close() de la ressource soit en la capturant pour la traiter soit en propageant cette exception pour laisser le soin de son traitement à la méthode appelante.
Exemple ( code Java 7 ) : |
Exemple ( code Java 7 ) : |
Résultat : |
Cette exemple ne se compile pas car l'exception pouvant être levée lors de l'invocation de la méthode close() n'est pas gérée.
Les exemples suivants utilisent deux exceptions personnalisées.
Exemple : |
Une ressource générique est définie : elle possède une méthode utiliser() et une redéfinition de la méthode close() car elle implémente l'interface AutoCloseable. Durant leur exécution, ces deux méthodes lèvent une exception.
Exemple ( code Java 7 ) : |
La ressource peut être utilisée dans du code compatible avec la version 6 de Java.
Exemple ( code Java 7 ) : |
Résultat : |
L'utilisation de la ressource avec l'instruction try-with-resource de Java 7 simplifie le code.
Exemple ( code Java 7 ) : |
Résultat : |
Le résultat est aussi légèrement différent : c'est l'exception levée lors de l'utilisation de la ressource qui est propagée et non l'exception levée lors de la fermeture de la ressource.
Si une exception est levée dans le bloc try et lors de la fermeture de la ressource, c'est l'exception du bloc try qui est propagée et l'exception levée lors de la fermeture est masquée.
Pour obtenir l'exception masquée, il est possible d'invoquer la méthode getSuppressed() de la classe Throwable sur l'instance de l'exception qui est propagée.
L'ARM fonctionne aussi si plusieurs ressources sont utilisées dans plusieurs instructions try-with-resources imbriquées.
Exemple ( code Java 7 ) : |
Résultat : |
Toutes les exceptions levées lors de la fermeture des ressources sont inhibées et peuvent être obtenues en invoquant la méthode getSuppressed().
Exemple ( code Java 7 ) : |
Résultat : |
La méthode getSuppressed() renvoie un tableau d'instances de Throwable qui contient les exceptions capturées lors de la fermeture des ressources et non propagées.
La classe Throwable est aussi enrichie d'un nouveau constructeur qui permet de prendre en compte ou non des exceptions supprimées. Si le booléen enableSuppression est à false, alors la méthode getSuppressed() renvoie un tableau vide et l'invocation de la méthode addSuppressed() n'aura aucun effet.
Il est possible de repropager une exception qui a été gérée par une instruction catch en utilisant le mot clé throw.
Avant Java 7, il n'était pas possible de relever une exception qui soit un super-type de l'exception capturée dans une clause catch : dans ce cas, le compilateur émettait une erreur "unreported exception Exception; must be caught or declared to be thrown".
Dans l'exemple ci-dessous, l'exception MonExceptionFille hérite de l'exception MonExceptionMere.
Exemple : |
Java 7 propose une analyse plus fine de la situation et permet de déclarer la levée d'une exception de type MonExceptionFille même si l'exception gérée et relevée est de type MonExceptionMere.
Exemple ( code Java 7 ) : |
Avant Java 7, cette portion de code aurait provoqué une erreur de compilation « unreported exception MonExceptionMere ». Ceci s'applique aussi pour plusieurs exceptions.
Exemple : |
Résultat : |
Le compilateur vérifie si le type d'une exception levée dans un bloc catch correspond à un des types d'exceptions déclaré dans la clause throws de la méthode. Si le type de l'exception capturée par la clause catch est Exception alors la clause throws ne peut pas être d'un de ses sous-types.
Exemple : |
Pour déclarer dans la clause throws les exceptions précises, il faut les capturer individuellement dans des clauses catch dédiées.
Exemple : |
Le compilateur de Java 7 effectue une analyse plus fine qui lui permet de connaitre précisément les exceptions qui peuvent être relevées indépendamment du type déclaré dans la clause catch qui va les capturer. Il est ainsi possible de capturer un super-type des exceptions qui seront relevées et déclarer le type précis des exceptions dans la clause throws.
Lorsqu'une clause catch déclare plusieurs types d'exceptions et relève l'exception dans son bloc de code, le compilateur vérifie :
Exemple ( code Java 7 ) : |
Attention cependant, il y a un cas ou la compatibilité du code antérieur n'est pas assurée avec Java 7 : ce cas concerne l'imbrication de deux try/catch quand le second bloc apparaît dans la clause catch du premier try. Le code du bloc try imbriqué lève une exception.
L'exemple ci-dessous se compile sans problème avec Java 6 :
Exemple : |
Ce même code ne se compile plus avec Java 7 car l'exception de type MonExceptionMere ne sera jamais traitée par la seconde clause catch.
Pour être compilé en Java 7, le code devra être modifié.
Java SE 7 propose une amélioration de la gestion des exceptions en permettant le traitement de plusieurs exceptions dans une même clause catch.
Il n'est pas rare d'avoir à dupliquer les mêmes lignes de code dans le bloc de code de plusieurs clauses catch().
Exemple : |
Avant Java 7, il était difficile d'éviter la duplication de code car chaque exception est de type différent.
Une solution utilisée pour éviter cette duplication est de catcher un super-type d'exception, généralement le type Exception. Cependant cette solution a plusieurs effets de bord, notamment le fait que le traitement s'appliquera à toutes les exceptions filles et englobera peut-être des exceptions qui auraient nécessité un traitement particulier. De plus, il ne sera pas possible de propager un autre type d'exception que celui capturé.
A partir de Java 7, la même portion de code est simplifiée : il suffit de déclarer les exceptions dans une même clause catch en les séparant par le caractère "|".
Exemple ( code Java 7 ) : |
Il n'est plus nécessaire de définir un bloc catch pour chaque exception et de dupliquer le code du bloc si c'est le même pour tous.
La clause catch peut contenir plusieurs types d'exceptions qui provoqueront l'exécution du bloc de code associé, chaque type d'exception est séparé d'un autre en utilisant le caractère barre verticale.
Il est possible d'utiliser plusieurs blocs catch notamment si les traitements des exceptions sont différents selon leur type.
Exemple ( code Java 7 ) : |
Si plusieurs types d'exceptions sont déclarés dans une clause catch alors la variable qui permettra un accès à l'exception concernée est implicitement déclarée final.
Le paramètre de la clause catch étant implicitement final, il n'est pas possible de réaffecter sa valeur dans le bloc de code dans lequel il est défini.
Exemple ( code Java 7 ) : |
Résultat : |
C'est le compilateur qui prend en charge la génération du code correspondant au support multi exceptions de la clause catch sans duplication de code.
L'avantage de cette gestion de plusieurs exceptions dans une clause catch n'est pas seulement syntaxique car il réduit la quantité de code produite. Le bytecode généré par le compilateur est meilleur comparé à celui produit pour plusieurs clauses catch équivalentes.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
||
Développons en Java v 2.40 Copyright (C) 1999-2023 Jean-Michel DOUDOUX. |