X. Chapitre 8. Erreurs et exceptions▲
Imaginons que tu aies oublié une accolade fermante dans ton code Java. Il se produirait une erreur de compilation, que tu pourrais corriger facilement. Mais il se produit aussi ce qu'on appelle des erreurs d'exécution (run-time errors), lorsque ton programme cesse tout à coup de fonctionner correctement. Par exemple, une classe Java lit un fichier contenant les scores du jeu. Que se passe-t-il si quelqu'un a supprimé ce fichier ? Le programme s'écroule-t-il en affichant un message d'erreur déroutant, ou reste-t-il en vie en affichant un message compréhensible comme : "Cher ami, pour une raison inconnue il m'est impossible de lire le fichier scores.txt. Merci de vérifier que ce fichier existe." ? Il est préférable de créer des programmes capables de gérer les situations inhabituelles. Dans beaucoup de langages de programmation, le traitement des erreurs dépend de la bonne volonté du programmeur. Mais Java rend obligatoire l'inclusion de code de gestion des erreurs. A défaut, les programmes ne peuvent même pas être compilés.
En Java, les erreurs d'exécution sont appelées des exceptions et le traitement des erreurs est appelé gestion des exceptions (exception handling). Tu dois placer le code susceptible de produire des erreurs dans des blocs try/catch (essayer/capturer). C'est comme si tu disais ceci à Java : Essaie de lire le fichier contenant les scores. Si quelque chose d'anormal se produit, capture l'erreur et exécute le code qui doit la gérer.
try
{
fichierScores.read
(
);
}
catch
(
IOException exception) {
System.out.println
(
"Cher ami, je ne peux pas lire le fichier scores.txt"
);
}
Nous apprendrons comment travailler avec des fichiers au Chapitre 9, mais pour l'instant familiarise-toi avec l'expression I/O ou input/output (entrée/sortie). Les opérations de lecture et d'écriture (sur disque ou sur d'autres périphériques) sont appelées I/O ; ainsi, IOException est une classe qui contient des informations relatives aux erreurs d'entrée/sortie.
Une méthode lève une exception en cas d'erreur. Différentes exceptions sont levées pour différents types d'erreurs. Si le bloc catch existe dans le programme pour un type particulier d'erreur, l'erreur est capturée et le programme se débranche sur le bloc catch pour exécuter le code qui s'y trouve. Le programme reste vivant et cette exception est considérée comme prise en charge.
L'instruction qui affiche le message dans le code ci-dessus n'est exécutée que dans le cas d'une erreur de lecture de fichier.
X-A. Lecture de la trace de la pile▲
S'il se produit une exception inattendue, non gérée par le programme, un message d'erreur multilignes est affiché à l'écran. Un tel message est appelé trace de la pile (stack trace). Si ton programme a appelé plusieurs méthodes avant de rencontrer un problème, la trace de la pile peut t'aider à suivre la trace de ton programme et à trouver la ligne qui a causé l'erreur.
Ecrivons le programme TestTracePile qui effectue volontairement une division par 0 (les numéros de ligne ne font pas partie du code).
1
class
TestTracePile {
2
TestTracePile
(
)
3
{
4
diviserParZéro
(
);
5
}
6
7
int
diviserParZéro
(
)
8
{
9
return
25
/
0
;
10
}
11
12
public
static
void
main
(
String[] args)
13
{
14
new
TestTracePile
(
);
15
}
16
}
La sortie de ce programme montre la séquence des appels de méthodes effectués jusqu'au moment où l'erreur d'exécution s'est produite. Lis cette sortie en remontant à partir de la dernière ligne.
Exception in thread "main"
java.lang.ArithmeticException: /
by zero
at TestTracePile.diviserParZéro
(
TestTracePile.java:9
)
at TestTracePile.<
init>(
TestTracePile.java:4
)
at TestTracePile.main
(
TestTracePile.java:14
)
Ceci signifie que le programme a commencé par la méthode main(), puis est entré dans init(), qui est un constructeur, et a ensuite appelé la méthode diviserParZéro(). Les nombres 14, 4 et 9 indiquent dans quelles lignes du programme ont eu lieu ces appels de méthodes. Enfin, une exception ArithmeticException a été levée : la ligne numéro 9 a essayé d'effectuer une division par 0.
X-B. Arbre généalogique des exceptions ▲
En Java, les exceptions sont aussi des classes. Certaines d'entre elles apparaissent dans l'arbre d'héritage ci-dessous :
Les sous-classes de la classe Exception sont appelées exceptions contrôlées (checked exceptions) et tu dois les traiter dans ton code.
Les sous-classes de la classe Error sont des erreurs fatales et le programme en cours d'exécution ne peut en général pas les gérer.
TropDeVélosException est un exemple d'exception créée par un développeur.
Comment un développeur est-il sensé savoir à l'avance si une méthode Java peut lever une exception et qu'un bloc try/catch est nécessaire ? Pas de soucis ; si tu appelles une méthode qui peut lever une exception, le compilateur Java affiche un message d'erreur comme celui-ci :
"LecteurDeScore.java"
: unreported exception:
java.io.IOException; must be caught or declared to be thrown at line 57
Bien sûr, tu es libre de lire la documentation Java qui décrit les exceptions qui peuvent être levées par une méthode donnée. La suite de ce chapitre explique comment gérer ces exceptions.
X-C. Bloc try/catch ▲
Il y a cinq mot-clés Java qui peuvent être utilisés dans le traitement des erreurs : try, catch, finally, throw et throws.
Après un bloc try, tu peux mettre plusieurs blocs catch, si tu penses que différentes erreurs sont susceptibles de se produire. Par exemple, quand un programme essaie de lire un fichier, le fichier peut ne pas être là, ce qui génère l'exception FileNotFoundException, ou le fichier peut être là mais le code continuer à le lire après avoir atteint la fin du fichier, ce qui génère l'exception EOFException. L'extrait de code suivant affiche des messages en bon français si le programme ne trouve pas le fichier contenant les scores ou atteint prématurément la fin du fichier. Pour toute autre erreur, il affiche le message "Problème de lecture de fichier" et une description technique de l'erreur.
public
void
chargerScores
(
) {
try
{
fichierScores.read
(
);
System.out.println
(
"Scores chargés avec succès"
);
}
catch
(
FileNotFoundException e) {
System.out.println
(
"Fichier Scores introuvable"
);
}
catch
(
EOFException e1) {
System.out.println
(
"Fin de fichier atteinte"
);
}
catch
(
IOException e2) {
System.out.println
(
"Problème de lecture de fichier"
+
e2.getMessage
(
));
}
}
Si la méthode read() (lire) échoue, le programme saute la ligne println() et essaie de trouver le bloc catch qui correspond à l'erreur. S'il le trouve, l'instruction println() appropriée est exécutée ; s'il n'y a pas de bloc catch correspondant à l'erreur, la méthode chargerScores() remonte l'exception à la méthode qui l'a appelée.
Si tu écris plusieurs blocs catch, tu dois faire attention à l'ordre dans lequel tu les écris si les exceptions que tu traites héritent les unes des autres. Par exemple, puisque EOFException est une sous-classe de IOException, tu dois placer le bloc catch de EOFException, la sous-classe, en premier. Si tu traitais IOException en premier, le programme n'atteindrait jamais les exceptions FileNotFound ou EOFException, car elles seraient interceptées par le premier catch.
Les fainéants pourraient programmer la méthode chargerScores() comme ceci :
public
void
chargerScores
(
) {
try
{
fichierScores.read
(
);
}
catch
(
Exception e) {
System.out.println
(
"Problème de lecture du fichier "
+
e.getMessage
(
));
}
}
C'est un exemple de mauvais style de code Java. Quand tu écris un programme, n'oublie jamais que quelqu'un d'autre pourrait le lire ; et tu ne veux pas avoir honte de ton code.
Les blocs catch reçoivent une instance de l'objet Exception contenant une courte explication du problème. La méthode getMessage() retourne cette information. Parfois, lorsque la description d'une erreur n'est pas claire, tu peux essayer la méthode toString() à la place :
catch
(
Exception exception) {
System.out.println
(
"Problème de lecture du fichier "
+
exception.toString
(
));
}
Si tu as besoin d'informations plus détaillées à propos de l'exception, utilise la méthode printStackTrace(). Elle affiche la séquence d'appels de méthodes qui a abouti à l'exception, de la même façon que dans l'exemple de la section Lecture de la trace de la pile.
Essayons de "tuer" le programme de calculatrice du Chapitre 6. Exécute la classe Calculatrice et entre au clavier les caractères abc. Appuie sur n'importe lequel des boutons d'opérations ; la console affiche quelque chose de ce genre :
Exception in thread "AWT-EventQueue-0"
java.lang.NullPointerException
at MoteurCalcul.actionPerformed
(
MoteurCalcul.java:43
)
at javax.swing.AbstractButton.fireActionPerformed
(
AbstractButton.java:1849
)
at javax.swing.AbstractButton$Handler.actionPerformed
(
AbstractButton.java:2169
)
at javax.swing.DefaultButtonModel.fireActionPerformed
(
DefaultButtonModel.java:420
)
C'est un exemple d'exception non gérée. Dans la méthode actionPerformed() de la classe MoteurCalcul, il y a ces lignes :
valeurAffichée =
// analyse la chaîne de caractères
formatNombres.parse
(
texteChampAffichage,
new
ParsePosition
(
0
) /* ne sert pas */
).
// puis donne sa valeur en tant que double
doubleValue
(
);
Si la variable texteChampAffichage ne représente pas une valeur numérique, la méthode parse() est incapable de la convertir dans un nombre et rend null. Du coup l'appel de la méthode doubleValue() lève une exception NullPointerException.
Gérons cette exception et affichons un message d'erreur qui explique le problème à l'utilisateur. Les lignes qui commencent par valeurAffichée doivent être placées dans un bloc try/catch ; Eclipse va t'y aider. Sélectionne tout le texte depuis valeurAffichée jusqu'au point-virgule après doubleValue()et clique sur le bouton droit de la souris. Dans le menu popup, sélectionne les sous-menus Source et Entourer d'un bloc try/catch. Réponds Oui à la question que te pose Eclipse et voilà ! Le code est modifié :
try
{
valeurAffichée =
// analyse la chaîne de caractères
formatNombres.parse
(
texteChampAffichage,
new
ParsePosition
(
0
) /* ne sert pas */
).
// puis donne sa valeur en tant que double
doubleValue
(
);
}
catch
(
RuntimeException e) {
// TODO Bloc catch auto-généré
e.printStackTrace
(
);
}
Remplace la ligne e.printStackTrace(); par ceci :
javax.swing.JOptionPane.showConfirmDialog
(
null
,
"Merci d'entrer un nombre."
, "Entrée incorrecte."
,
javax.swing.JOptionPane.PLAIN_MESSAGE);
return
;
Nous nous sommes débarrassés des messages d'erreur déroutants de la trace de la pile, pour afficher à la place le message facile à comprendre "Merci d'entrer un nombre" :
Maintenant, l'exception NullPointerException est gérée.
X-D. Le mot-clé throws▲
Dans certains cas, il est plus approprié de traiter l'exception non pas dans la méthode où elle se produit, mais dans la méthode appelante.
Dans de tels cas, la signature de la méthode doit déclarer (avertir) qu'elle peut lever une exception particulière. On utilise pour cela le mot-clé spécial throws. Reprenons notre exemple de lecture de fichier. Puisque la méthode read() peut déclencher une exception IOException, tu dois la gérer ou la déclarer. Dans l'exemple suivant, nous déclarons que la méthode chargerTousLesScores() peut émettre une IOException :
class
MonSuperJeu {
void
chargerTousLesScores
(
) throws
IOException {
// …
// N'utilise pas try/catch si tu ne gères pas
// d'exceptions dans cette méthode
fichier.read
(
);
}
public
static
void
main
(
String[] args) {
MonSuperJeu jeu =
new
MonSuperJeu
(
);
System.out.println
(
"Liste des scores"
);
try
{
// Puisque la méthode chargerTousLesScores() déclare
// une exception, nous la gérons ici
jeu.chargerTousLesScores
(
);
}
catch
(
IOException e) {
System.out.println
(
"Désolé, la liste des scores n'est pas disponible"
);
}
}
}
Comme nous n'essayons même pas de capturer d'exception dans la méthode chargerTousLesScores(), l'exception IOException est propagée à sa méthode appelante, main(). Celle-ci doit maintenant gérer cette exception.
X-E. Le mot-clé finally▲
Le code inclus dans un bloc try/catch peut se terminer de trois façons :
- Le code à l'intérieur du bloc try est exécuté avec succès et le programme se poursuit.
- Le code à l'intérieur du bloc try rencontre une instruction return et le programme sort de la méthode.
- Le code à l'intérieur du bloc try lève une exception et le contrôle passe au bloc catch correspondant. Soit celui-ci gère l'erreur et l'exécution de la méthode se poursuit ; soit il réémet l'exception à destination de la méthode appelante.
Si un morceau de code doit être exécuté quoi qu'il arrive, il faut le placer dans un bloc finally :
try
{
fichier.read
(
);
}
catch
(
Exception e) {
e.printStackTrace
(
);
}
finally
{
// Le code qui doit toujours être exécuté,
// par exemple file.close(), est placé ici.
}
Le code ci-dessus doit fermer le fichier indépendamment du succès ou de l'échec de l'opération de lecture. En général, on trouve dans le bloc finally le code qui libère des ressources de l'ordinateur, par exemple la déconnexion d'un réseau ou la fermeture d'un fichier.
Si tu n'as pas l'intention de traiter les exceptions dans la méthode courante, elles sont propagées à l'appelant. Dans ce cas, tu peux utiliser finally même si tu n'as pas de bloc catch :
void
maMéthode
(
) throws
IOException {
try
{
// Place ici le code qui lit le fichier.
}
finally
{
// Place ici le code qui ferme le fichier.
}
}
X-F. Le mot-clé throw▲
Si une exception se produit dans une méthode, mais que tu penses que c'est à l'appelant de la traiter, il suffit de la réémettre vers celui-ci. Parfois, tu peux vouloir capturer une exception mais en lever une autre avec une description différente de l'erreur, comme dans le bout de code ci-dessous.
L'instruction throw est utilisée pour lever des exceptions en lançant des objets Java. L'objet lancé par un programme doit être émissible (throwable). C'est-à-dire que tu ne peux lancer que les objets qui sont des sous-classes directes ou indirectes de la classe Throwable - ce que sont toutes les exceptions Java.
Le fragment de code suivant montre comment la méthode chargerTousLesScores() capture une IOException, crée un nouvel objet Exception avec une description plus sympathique de l'erreur, et le lance vers la méthode main(). Du coup, pour que la méthode main() puisse être compilée, il faut ajouter une ligne qui appelle chargerTousLesScores() dans le bloc try/catch, car cette méthode peut émettre un objet Exception qui doit être traité ou réémis. La méthode main() ne devant lever aucune exception, elle doit traiter celle-ci.
class
ListeDesScores {
// Ce code doit être complété pour pouvoir le compiler
static
void
chargerTousLesScores
(
) throws
Exception {
try
{
fichier.read
(
); // Cette ligne peut lever
// une exception
}
catch
(
IOException e) {
throw
new
Exception
(
"Cher ami, le fichier des scores a un problème."
);
}
}
public
static
void
main
(
String[] args) {
System.out.println
(
"Scores"
);
try
{
chargerTousLesScores
(
);
}
catch
(
Exception e1) {
System.out.println
(
e1.getMessage
(
));
}
}
}
Si une erreur fichier se produit, la méthode main() la prend en charge et l'appel à e1.getMessage() renvoie le message "Cher ami …"
X-G. Création de nouvelles exceptions ▲
Les programmeurs peuvent aussi créer des classes d'exception Java entièrement nouvelles. Une telle classe doit être dérivée de l'une des classes d'exception Java. Disons que tu travailles dans une affaire de vente de bicyclettes et que tu aies besoin de valider les commandes des clients. Le nombre de vélos que tu peux mettre dans ta camionnette dépend de leur modèle. Par exemple, tu ne peux pas y mettre plus de trois vélos du modèle "Eclair". Crée une nouvelle sous-classe de Exception et si quelqu'un essaie de commander plus de trois de ces bicyclettes, lève cette exception :
class
TropDeVélosException extends
Exception {
// Constructeur
TropDeVélosException
(
) {
// Appelle simplement le constructeur de la superclasse
// et lui passe le message d'erreur à afficher
super
(
"Impossible de livrer autant de vélos en une fois."
);
}
}
Cette classe a juste un constructeur qui prend le message décrivant l'erreur et le passe à sa superclasse pour qu'elle le stocke. Quand un bloc catch reçoit cette exception, il peut savoir ce qu'il se passe exactement en appelant la méthode getMessage().
Imagine qu'un utilisateur sélectionne plusieurs bicyclettes d'un certain modèle dans la fenêtre EcranCommande et appuie sur le bouton Commander. Comme tu le sais depuis le Chapitre 6, cette action aboutit à l'appel de la méthode actionPerformed(), laquelle vérifie si la commande peut être livrée. L'exemple de code suivant montre comment la méthode vérifierCommande() déclare qu'elle est susceptible de lever l'exception TropDeVélosException. Si la commande ne rentre pas dans la camionnette, la méthode lève l'exception, le bloc catch l'intercepte et affiche un message d'erreur dans le champ textuel de la fenêtre.
class
EcranCommande implements
ActionListener {
// Place ici le code pour créer les composants de la fenêtre.
public
void
actionPerformed
(
ActionEvent evt) {
// L'utilisateur a cliqué sur le bouton Commander
String modèleChoisi =
champTexteModèle.getText
(
);
String quantitéChoisie =
champTexteQuantité.getText
(
);
int
quantité =
Integer.parseInt
(
quantitéChoisie);
try
{
commandeVélos.vérifierCommande
(
modèleChoisi,
quantitéChoisie);
// Cette ligne sera sautée en cas d'exception
champTexteConfirmationCommande.setText
(
"Votre commande est prête."
);
}
catch
(
TropDeVélosException e) {
champTexteConfirmationCommande.setText
(
e.getMessage
(
));
}
}
void
vérifierCommande
(
String modèleVélo, int
quantité)
throws
TropDeVélosException {
// Ecris le code qui vérifie que la quantité demandée
// du modèle sélectionné entre bien dans la camionnette.
// Si ça n'entre pas, faire ce qui suit :
throw
new
TropDeVélosException (
"Impossible de livrer "
+
quantité +
" vélos du modèle "
+
modèleVélo +
" en une fois."
);
}
}
Dans un monde parfait, tous les programmes fonctionneraient parfaitement, mais il faut être réaliste et être prêt à réagir aux situations inattendues. Java nous aide vraiment en nous forçant à écrire un code adapté à ces situations.
X-H. Autres lectures▲
X-I. Exercices▲
X-J. Exercices pour les petits malins▲
Modifie l'application de l'exercice précédent en remplaçant le champ textuel Modèle de vélo par une boîte de liste déroulante contenant plusieurs modèles, afin que l'utilisateur puisse choisir dans la liste au lieu d'entrer son choix au clavier. Vas lire en ligne ce qui est dit du composant Swing JComboBox et de ItemListener pour traiter les événements correspondant au choix d'un modèle par l'utilisateur. |