Une fois un programme accepté par le compilateur, il n’est pas garanti qu’aucune erreur se produira. Bien au contraire, l’expérience nous apprend que des erreurs se produiront.
Certaines erreurs empêchent l’exécution de se poursuivre dès qu’elles sont commises. Par exemple, si nous cherchons à accéder à la quatrième case d’un tableau qui ne comprend que trois cases, le système d’exécution ne peut plus rien faire de raisonnable et il fait échouer le programme.
Nous obtenons,
% java Test Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3 at Test.main(Test.java:4)
Le message affiché nous signale que notre programme a échoué
à cause d’une exception dont le nom
est ArrayIndexOutOfBoundsException
(qui semble à peu près
clair). L’affichage offre quelques bonus, l’indice fautif, et surtout
la ligne du programme qui a lancé l’exception.
D’autres exemples classiques d’erreur sanctionnée par une exception
sont le déréférencement de null
(sanctionné par
NullPointerException
) ou la division par zéro
(sanctionnée par ArithmeticException
).
Lancer une exception ne signifie pas du tout que le programme s’arrête
immédiatement. On peut expliquer le mécanisme ainsi :
l’exception remplace un résultat attendu (par exemple le résultat
d’une division) et ce résultat « exceptionnel » est propagé à travers
toutes les méthodes en attente d’un résultat, jusqu’à la méthode
main. Le système d’exécution de Java affiche alors un message pour
signaler qu’une exception a été lancée.
Pour préciser cet effet de propagation
considérons l’exemple suivant d’un programme Square
.
Le programme Square
est censé afficher le carré de l’entier
passé sur la ligne de commande.
% java Square 11 121
Mais si nous donnons un argument qui n’est pas la représentation d’un entier en base dix, nous obtenons ceci :
% java Square bonga Exception in thread "main" java.lang.NumberFormatException: For input string: "bonga" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48) at java.lang.Integer.parseInt(Integer.java:447) at java.lang.Integer.parseInt(Integer.java:497) at Square.read(Square.java:3) at Square.main(Square.java:7)
Où on voit bien la suite des appels en attente que l’exception a du remonter avant d’atteindre main.
Le mécanisme des exceptions est intégré dans le langage, ce qui veut dire que nous pouvons manipuler les exceptions. Ici, la survenue de l’exception trahit une erreur de programmation : en réaction à une entrée incorrecte le programme devrait normalement avertir l’utilisateur et donner une explication. Pour ce faire on attrape l’exception à l’aide d’une instruction spécifique.
Et on obtient alors.
% java Square bonga Usage : java Square nombre
Sans trop rentrer dans les détails,
l’instruction
try {
instrtry } catch (
E e ) {
instrfail }
s’exécute comme l’instruction
instrtry, mais si une exception E est
lancée lors de cette exécution, alors
l’instruction instrfail est exécutée.
En ce cas, l’exception attrapée disparaît.
Dans l’instruction instrfail on peut accéder à
l’exception attrapée par le truchement de la variable e.
Cela permet par exemple d’afficher l’exception coupable par
e.toString()
comme ici,
ou plus fréquemment le message contenu dans l’exception
par e.getMessage()
.
Et on obtient alors
% java Square bonga Usage : java Square nombre Échec sur : java.lang.NumberFormatException: For input string: "bonga"
Pour être tout à fait complet sur l’exemple de Square
, il y a
encore un problème. Nous écrivons read(arg[0])
sans
vérification que le tableau arg
possède bien un élément.
Nous devons aussi envisager d’attraper une exception
ArrayIndexOutOfBoundsException
. Une solution possible
est d’écrire :
On note qu’il peut en fait y avoir plusieurs clauses catch
.
Les deux clauses catch
appellent ici la méthode usage
,
qui affiche un bref message résumant l’usage correct du
programme et arrête l’exécution.
Lorsque l’on écrit un composant quelconque, c’est-à-dire du code réutilisable par autrui, on se trouve parfois dans la situation de devoir signaler une erreur. Il faut alors procéder exactement comme le système d’exécution de Java et lancer une exception. Ainsi l’utilisateur de notre composant a l’opportunité de pouvoir réparer son erreur. Au pire, un message compréhensible sera affiché.
Supposons par exemple une classe des piles d’entiers. Une tentative de de dépiler sur une pile vide se solde par une erreur, on lance alors une exception par l’instruction throw.
Il apparaît clairement qu’une exception est un objet d’une classe
particulière, ici la classe Error.
En raison de sa simplicité nous utilisons systématiquement Error
dans nos exemples.
Si vous prenez la peine de lire la documentation vous verrez que
dans l’esprit des auteurs de la bibliothèque, les
exceptions Error
ne sont pas censées être attrapées.
An Error […] indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions.
Pour signaler des erreurs réparables, la documentation encourage plutôt la classe Exception.
The class Exception and its subclasses […] indicates conditions that a reasonable application might want to catch.
En fait, il vaut mieux ne pas lancer Exception
, mais plutôt une
exception par nous définie (comme une sous-classe
de Exception
) ; ceci afin d’éviter les interférences avec les
exceptions lancées par les méthodes de la bibliothèque.
Nous procédons donc en deux temps, d’abord déclaration de la classe de notre exception.
La classe Empty
est définie comme un membre statique
de la classe Stack
(oui, c’est possible), de sorte que
l’on la désigne normalement comme Stack.Empty
.
Ce détail mis à part, il s’agit d’une déclaration de classe normale,
mais qui ne possède aucun membre en propre. Le code indique simplement
que Empty
est une sous-classe (voir B.2.3
et B.1.5) de Exception
. Un constructeur par défaut
Empty ()
est implicitement fourni, qui se contente
d’appeler le constructeur Exception ()
.
Ensuite, notre exception est lancée comme d’habitude.
Mais alors, la compilation de la classe Stack
échoue.
% javac Stack.java Stack.java:13: unreported exception Stack.Empty; must be caught or declared to be thrown if (isEmpty()) throw new Empty () ; ^ 1 error
En effet, Java impose de déclarer qu’une méthode lance une exception
Exception
(mais pas une exception Error
).
On déclare que la méthode pop
lance l’exception Empty
par le mot clé throws
(noter le « s »).
On peut considérer que les exceptions lancées par une méthode font partie de sa signature (voir B.3.3), c’est-à-dire font partie des informations à connaître pour pouvoir appeler la méthode.
Les déclarations obligatoires des exceptions lancées sont assez
contraignantes, en effet il faut tenir compte non seulement des
exceptions lancées directement, mais aussi de celles
lancées indirectement par les méthodes appelées.
Ainsi si on conçoit une méthode remove(int n)
pour enlever n
éléments d’une pile, on doit tenir compte de l’exception
Empty
éventuellement lancée par pop
.
Noter qu’il est également possible pour remove
de ne pas
signaler que la pile comportait moins de n éléments.
Cela revient à attraper l’exception Empty
,
et c’est d’autant plus facile que remove
ne renvoie pas de résultat.
Un exemple plus sophistiqué que remove
de la section précédente
est celui d’une méthode
int sum(int n)
qui conceptuellement dépile les n premiers éléments
et renvoie leur somme. Dans le cas où la pile contient moins de n
éléments on demande que la pile ne soit pas modifiée et que sum
lance une exception spécifique TooSmall
.
On observe que la nature de l’exception attrapée dénonce la méthode
qui l’a lancée et donc commande la remise en pile de x
ou non.