Les exceptions

Chapitres traités   


Problèmes lors de l'exécution

Même lorsqu'un programme est au point, certaines circonstances exceptionnelles peuvent compromettre la poursuite de son exécution ; il peut s'agir par exemple de données incorrectes ou de la rencontre d'une fin de fichier prématurée (alors qu'on a besoin d'informations supplémentaires pour continuer le traitement).

Bien entendu, on peut toujours essayer d'examiner toutes les situations possibles au sein du programme et prendre les décisions qui s'imposent. Mais outre le fait que le concepteur du programme risque d'omettre certaines situations, la démarche peut devenir très vite fastidieuse et les codes quelque peu complexes. Le programme peut être rendu quasiment illisible si sa tâche principale est masquée par de nombreuses instructions de traitement de circonstances exceptionnelles.

Par ailleurs, dans des programmes relativement importants, il est fréquent que le traitement d'une anomalie ne puisse pas être fait par la méthode l'ayant détectée, mais seulement par une méthode ayant provoqué son appel. Cette dissociation entre la détection d'une anomalie et son traitement peut obliger le concepteur à utiliser des valeurs de retour de méthode servant de "compte rendu". Là encore, le programme peut très vite devenir complexe ; de plus, la démarche ne peut pas s'appliquer à des méthodes sans valeur de retour donc, en particulier, aux constructeurs.

La situation peut encore empirer lorsque l'on développe des classes réutilisables destinées à être exploitées par de nombreux programmes.

Java dispose d'un mécanisme très souple nommé gestion d'exception, qui permet à la fois :

  1. de dissocier la détection d'une anomalie de son traitement,
  2. de séparer la gestion des anomalies du reste du code, donc de contribuer à la lisibilité des programmes.

D'une manière générale, une exception est une rupture de séquence déclenchée par une instruction throw comportant une expression de type classe. Il y a alors branchement à un ensemble d'instructions nommé "gestionnaire d'exception". Le choix du bon gestionnaire est fait en fonction du type de l'objet mentionné à throw (de façon comparable au choix d'une fonction surdéfinie).

Choix du chapitre Comment déclencher une exception avec throw

Considérons une classe Point, munie d'un constructeur à deux arguments et d'une méthode affiche. Supposons que l'on ne souhaite manipuler que des points ayant des coordonnées non négatives. Nous pouvons, au sein du constructeur, vérifier la validité des paramètres fournis. Lorsque l'un d'entre eux est incorrect, nous "déclenchons" (on emploie aussi les verbes "lancer" ou "lever") une exception à l'aide de l'instruction throw. A celle-ci, nous devons fournir un objet dont le type servira ultérieurement à identifier l'exception concernée. Nous créons donc (un peu artificiellement) une classe que nous nommerons ErrConst. Java impose que cette classe dérive de la classe standard Exception. Pour l'instant, nous n'y plaçons aucun membre (mais nous le ferons dans d'autres exemples) :

class ErrConst extends Exception { }

Pour lancer une exception de ce type au sein de notre constructeur, nous fournirons à l'instruction throw un objet de type ErrConst, par exemple de cette façon :

throw new ErrConst(); Objet anonyme

En définitive, le constructeur de notre classe Point peut se présenter ainsi :

Notez la présence de throws ErrConst, dans l'en-tête du constructeur, qui précise que la méthode est susceptible de déclencher une exception de type ErrConst. Cette indication est obligatoire en Java, à partir du moment où l'exception en question n'est pas traitée par la méthode elle-même.



Choix du chapitre Utilisation d'un gestionnaire d'exception

Disposant de notre classe Point, voyons maintenant comment procéder pour gérer convenablement les éventuelles exceptions de type ErrConst que son emploi peut déclencher. Pour ce faire, il faut :

  1. inclure dans un bloc particulier dit "bloc try" les instructions dans lesquelles on risque de voir déclenchée une telle exception ;
  2. faire suivre ce bloc de la définition des différents gestionnaires d'exception (ici, un seul suffit). Chaque définition de gestionnaire est précédée d'un en-tête introduit par le mot clé catch (comme si catch était le nom d'une méthode gestionnaire).

Si une exception est déclenchée par une instruction situé dans le bloc try, alors :

  1. Le programme abandonne l'exécution du code restant dans le bloc try.
  2. Le programme exécute le code du gestionnaire situé dans la clause catch.

Si aucune des instructions du bloc try ne lance une exception, le programme ignore la clause catch.

Dans l'exemple proposé, tout se passe bien jusqu'à ce la ligne a = new Point(-3, 5); L'exécution de cet instruction déclenche alors l'exception ErrConst, le programme se détourne et exécute donc le code relatif au gestionnaire associé placé dans la clause catch.

Ici, le gestionnaire se contente d'afficher un message et d'interrompre l'exécution du programme en appelant la méthode standard System.exit (la valeur de l'argument est transmis au système d'exploitation qui peut éventuellement l'utiliser comme "compte rendu"; lorsqu'il s'agit de la valeur -1, le système utilise le canal standard d'erreur, et par défaut c'est souvent l'écran).

Ce premier exemple est très restrictif pour différentes raisons :

  1. on n'y déclenche et on ne traite qu'un seul type d'exception ;
  2. le gestionnaire d'exception ne reçoit aucune information ; plus exactement, il reçoit un objet sans valeur qu'il ne cherche pas à utiliser ;
  3. nous n'exploitons pas les fonctionnalités de la classe Exception dont dérive notre classe ErrConst ;
  4. le gestionnaire d'exception se contente d'interrompre le programme alors qu'il est possible de poursuivre l'exécution.

Choix du chapitre Gestion de plusieurs exceptions

Voyons maintenant un exemple plus complet dans lequel on peut déclencher et traiter deux types d'exceptions. Pour ce faire, nous considérons une classe Point munie :

  1. du constructeur précédent, déclenchant toujours une exception ErrConst,
  2. d'une méthode déplace qui s'assure que le déplacement ne conduit pas à une coordonnée négative ; si tel est le cas, elle déclenche une exception ErrDepl (on crée donc, ici encore, une classe ErrDepl) :

Lors de l'utilisation de notre classe Point, nous pouvons détecter les deux exceptions potentielles ErrConst et ErrDepl en procédant ainsi (ici, nous nous contentons comme précédemment d'afficher un message et d'interrompre l'exécution) :

Dans cet exemple, nous provoquons volontairement une exception ErrDepl ainsi qu'une exception ErrConst.

Bien entendu, comme la première exception ErrDepl provoque la sortie du bloc try (et, de surcroît, l'arrêt de l'exécution), nous n'avons aucune chance de mettre en évidence celle qu'aurait provoquée la tentative de construction d'un point par l'appel new Point(-3, 5);



Choix du chapitre Transmission d'information au gestionnaire d'exception

On peut transmettre une information au gestionnaire d'exception :

  1. par le biais de l'objet fourni dans l'instruction throw,
  2. par l'intermédiaire du constructeur de la classe Exception.

Par l'objet fourni à l'instruction throw

Comme nous l'avons vu, l'objet fourni à l'instruction throw sert à choisir le bon gestionnaire d'exception. Mais il est aussi récupéré par le gestionnaire d'exception sous la forme d'argument, de sorte qu'on peut l'utiliser pour transmettre une information. Il suffit pour cela de prévoir les attributs appropriés dans la classe correspondante (on peut aussi y trouver des méthodes).

Voici une adaptation de l'exemple précédent, dans lequel nous dotons la classe ErrConst d'attributs abs et ord permettant de transmettre les coordonnées reçues par le constructeur de Point. Leurs valeurs sont fixées lors de la construction d'un objet de type ErrConst et elles sont récupérées directement par le gestionnaire (car les attributs ont été prévus publics ici, pour simplifier les choses).

Par le constructeur de la classe Exception

Dans certains cas, on peut se contenter de transmettre un "message" au gestionnaire, sous forme d'une information de type chaîne. La méthode précédente reste bien sûr utilisable, mais on peut aussi exploiter une particularité de la classe Exception (dont dérive obligatoirement votre classe). En effet, celle-ci dispose d'un constructeur à un argument de type String dont on peut récupérer la valeur à l'aide de la méthode getMessage (dont héritera votre classe).

Pour bénéficier de cette facilité, il suffit de prévoir dans la classe exception ErrConst, un constructeur à un argument de type String, qu'on retransmettra au constructeur de la super-classe Exception. Voici une adaptation dans ce sens du programme précédent.

Choix du chapitre Poursuite de l'exécution

Dans tous les exemples précédents, le gestionnaire d'exception mettait fin à l'exécution du programme en appelant la méthode System.exit. Cela n'est pas une obligation ; en fait, après l'exécution des instructions du gestionnaire, l'exécution se poursuit simplement avec les instructions suivant le bloc try (plus précisément, le dernier bloc catch associé à ce bloc try). En voici un exemple et qui reprend également les deux classes d'exception déjà traitées plus haut :

Choix du chapitre Choix du gestionnaire d'exception

Lorsqu'une exception est déclanchée dans le bloc try, on recherche parmi les différents gestionnaires associés celui qui correspond à l'objet mentionné à throw. L'examen a lieu dans l'ordre où les gestionnaires apparaissent. On sélectionne le premier qui est soit du type exact de l'objet, soit d'un type de base (polymorphisme). Cette possibilité peut être exploitée pour regrouper plusieurs exceptions qu'on souhaite traiter plus ou moins finement. Supposons par exemple que les exceptions ErrConst et ErrDepl sont dérivées d'une même classe ErrPoint :

Considérons une méthode f quelconque (peut importe à qu'elle classe elle appartient) déclenchant des exceptions de type ErrConst et ErrDepl :

Dans un programme utilisant la méthode f, on peut gérer les exceptions qu'elle est susceptible de déclencher de cette façon :

Mais on peut aussi les gérer ainsi :

Choix du chapitre Cheminement des exceptions et la clause throws

Lorsqu'une méthode déclenche une exception, on cherche tout d'abord un gestionnaire dans l'éventuel bloc try contenant l'instruction throw correspondant. Si l'on n'en trouve pas ou si aucun bloc try n'est prévu à ce niveau, on poursuit la recherche dans un éventuel bloc try associé à l'instruction d'appel dans une méthode appelante, et ainsi de suite.

Nous avons déjà noté la présence de la clause throws dans l'en-tête de certaines méthodes (constructeur de Point, méthode déplace). D'une manière générale, Java impose la règle suivante :

Règle : Toute méthode susceptible de déclencher une exception qu'elle ne traite pas localement doit mentionner son type dans une clause throws figurant dans son en-tête. Lorsqu'il existe plusieurs exceptions, il faut toutes les mentionner en les séparant par l'opérateur virgule.

On notera que si aucune clause throws ne figure dans l'en-tête de la méthode main, on est certain que toutes les exceptions sont prises en compte. En revanche, si une clause throws y figure, les exceptions mentionnées ne seront pas prises en compte. Comme il n'y a aucun bloc try englobant, on aboutit alors à une erreur d'exécution précisant l'exception concernée.

Choix du chapitre Redéclenchement d'une exception

Dans un gestionnaire d'exception, il est possible de demander que, malgré son traitement, l'exception soit retransmise à un niveau englobant, comme si elle n'avait pas été traitée. Il suffit pour cela de la relancer en appelant à nouveau l'instruction throw.

Cette possibilité de re-déclenchement d'une exception s'avère très précieuse lorsque l'on ne peut résoudre localement qu'une partie du problème posé.

Choix du chapitre Le bloc finally

Nous avons vu que le déclenchement d'une exception provoque un branchement inconditionnel au gestionnaire, à quelque niveau qu'il se trouve. Il n'est pas possible de revenir à la suite de l'instruction ayant provoqué l'exception (sauf si elle est la seule ou la dernière d'un bloc try).

Cependant, Java permet d'introduire, à la suite d'un bloc try, un bloc particulier d'instructions qui seront toujours exécutées :

  1. soit après la fin "naturelle" du bloc try, si aucune exception n'a été déclenchée .
  2. soit après le gestionnaire d'exception (à condition, bien sûr, que ce dernier n'ait pas provoqué d'arrêt de l'exécution).

Ce bloc est introduit par le mot clé finally et doit obligatoirement être placé après le dernier gestionnaire. Voici un exemple de code, accompagné de deux exemples d'exécutions :

Choix du chapitre Les exceptions standard

Java fourni de nombreuses classes prédéfinies dérivées de la classe Exception, qui sont utilisées par certaines méthodes standard ; par exemple, la classe IOException et ses dérivées sont utilisées par les méthodes d'entrées-sorties. Certaines classes exception sont même utilisées par la machine virtuelle à la rencontre de situations anormales telles qu'un indice de tableau hors limites, une taille de tableau négative...

Ces exceptions standard se classent en deux catégories :

  1. Les exceptions explicites (on dit aussi sous contrôle) correspondent à ce que nous venons d'étudier. Elles doivent être traitées par une méthode, ou bien être mentionnées dans la clause throws.
  2. Les exceptions implicites (ou hors contrôle) n'ont pas à être mentionnées dans une clause throw et on n'est pas obligé de les traiter (mais on peut quand même le faire).

En fait, cette classification sépare les exceptions susceptibles de se produire n'importe où dans le code de celles dont on peut distinguer les sources potentielles. Par exemple, un débordement d'indice ou une division entière par zéro peuvent se produire presque partout dans le programme ; ce n'est pas le cas d'une erreur de lecture de fichier, qui ne peut survenir que si l'on fait appel à des méthodes bien précises.

Voici la liste des exceptions standard

Voici un exemple de programme qui détecte les exceptions standard NegativeArraySizeException et ArrayIndexOutOfBoundsException et qui utilise la méthode getMessage pour afficher le message correspondant. Il est accompagné de deux exemples d'exécution.

Nous pourrions écrire le programme précédent sans détection d'exceptions. Comme les exceptions concernées sont implicites, il n'est pas nécessaire de les déclarer dans throws. Dans ce cas, les deux exemples d'exécution conduiraient à un message d'erreur et à l'abandon du programme.


Pour bien maîtriser les exceptions, vous allez mettre en oeuvre le programme présenté ci-contre. Le but de ce petit logiciel, est de tracer une seule forme sur la partie principale de la fenêtre, soit un cercle, soit un carré. Dès le démarrage, un cercle doit être tracé aux coordonnées de <50, 50> (x et y) avec un diamètre de 100 (taille). Il est possible, soit de changer de forme, soit de changer de coordonnées, soit de changer de dimension. Pour réaliser les deux dernières opérations, il suffit de placer votre valeur dans la bonne zone d'édition et de valider à l'aide de la touche "Entrée". Si la valeur est correcte, la forme doit être réafficher immédiatement avec les nouvelles propriétés.

Par rapport à la taille de la fenêtre, la forme doit toujours être visible entièrement, ce qui implique qu'il existe des valeurs incorrectes comme, par exemple, lorsqu'on propose une abscisse de x = 20 alors que le diamètre est de 100. Si on réalisé le tracé avec ces dernières caractéristiques, une partie du cercle serait tronquée.

Vous allez utiliser le mécanisme de gestion des exceptions pour éviter que le programme s'arrête inopinément, pour avertir l'utilisateur du problème rencontré et pour remettre en état le système pour que le logiciel soit de nouveau opérationnel.

Vous avez ci-dessus un scénario qui montre la saisie d'une valeur incorrecte. Lorsque je propose la valeur -1 à y, une exception adaptée doit être levée. Ensuite, afin d'éviter que le programme s'arrête, l'exception doit être capturée. Lors de la capture, vous proposez l'affichage dans la barre d'état de votre fenêtre, le message d'erreur correspondant et vous replacer l'ancienne valeur dans la zone d'édition qui a provoqué l'exception.

En fait, six type d'erreur ont été recensés. Voici la liste des messages correspondant :

Pour vous aider, vous avez, ci-dessous, le codage du fichier "Forme.java".

Il existe un cas de figure où une forme risque de ne pas être visible, c'est lorsque nous réduisont la taille de la fenêtre. Ce cas n'est pas à prendre en compte. Si toutefois vous désirez prendre en considération ce problème, vous devrez faire un changement automatique de coordonnées de la forme (ou en dernier ressort sa dimension) pour qu'elle puisse toujours apparaître dans la zone d'affichage. Utilisez, dans ce cas, les exceptions déjà créées.