Voici encore une notion très importante en programmation. Une exception est une erreur se produisant dans un programme qui conduit le plus souvent à l'arrêt de celui-ci. Il vous est sûrement déjà arrivé d'obtenir un gros message affiché en rouge dans la console d'Eclipse : eh bien, cela a été généré par une exception… qui n'a pas été capturée.
Le fait de gérer les exceptions s'appelle aussi « la capture d'exception ». Le principe consiste à repérer un morceau de code (par exemple, une division par zéro) qui pourrait générer une exception, de capturer l'exception correspondante et enfin de la traiter, c'est-à-dire d'afficher un message personnalisé et de continuer l'exécution.
Bon, vous voyez maintenant ce que nous allons aborder dans ce chapitre… Donc, allons-y !
- Le bloc try{...} catch{...}
- Les exceptions personnalisées
- La gestion de plusieurs exceptions
- Depuis Java 7 : le multi-catch
Le bloc try{...} catch{...}
Pour vous faire comprendre le principe des exceptions, je dois tout d'abord vous informer que Java contient une classe nommée Exception
dans laquelle sont répertoriés différents cas d'erreur. La division par zéro dont je vous parlais plus haut en fait partie ! Si vous créez un nouveau projet avec seulement la classe main
et y mettez le code suivant :
1 2 3 | int j = 20, i = 0; System.out.println(j/i); System.out.println("coucou toi !"); |
… vous verrez apparaître un joli message d'erreur Java (en rouge) comme celui de la figure suivante.
Mais surtout, vous devez avoir constaté que lorsque l'exception a été levée, le programme s'est arrêté ! D'après le message affiché dans la console, le nom de l'exception qui a été déclenchée est ArithmeticException
. Nous savons donc maintenant qu'une division par zéro est une ArithmeticException
. Nous allons pouvoir la capturer, avec un bloc try{…}catch{…}
, puis réaliser un traitement en conséquence. Ce que je vous propose maintenant, c'est d'afficher un message personnalisé lors d'une division par 0. Pour ce faire, tapez le code suivant dans votre main
:
1 2 3 4 5 6 7 8 9 10 | public static void main(String[] args) { int j = 20, i = 0; try { System.out.println(j/i); } catch (ArithmeticException e) { System.out.println("Division par zéro !"); } System.out.println("coucou toi !"); } |
En exécutant ce code, vous obtiendrez le résultat visible à la figure suivante.
Voyons un peu ce qui se passe :
- Nous initialisons deux variables de type
int
, l'une à 0 et l'autre à un nombre quelconque. - Nous isolons le code susceptible de lever une exception :
System.out.println(j/i);
. - Une exception de type
ArithmeticException
est levée lorsque le programme atteint cette ligne. - Notre bloc
catch
contient justement un objet de typeArithmeticException
en paramètre. Nous l'avons appelée
. - L'exception étant capturée, l'instruction du bloc
catch
s'exécute ! - Notre message d'erreur personnalisé s'affiche alors à l'écran.
Vous vous demandez sûrement à quoi sert le paramètre de la clause catch
. Il permet de connaître le type d'exception qui doit être capturé. Et l'objet — ici, e
— peut servir à préciser notre message grâce à l'appel de la méthode getMessage()
. Faites à nouveau ce test, en remplaçant l'instruction du catch
par celle-ci :
1 | System.out.println("Division par zéro !" + e.getMessage()); |
Vous verrez que la fonction getMessage()
de notre objet ArithmeticException
nous précise la nature de l'erreur.
Je vous disais aussi que le principe de capture d'exception permettait de ne pas interrompre l'exécution du programme. En effet, lorsque nous capturons une exception, le code présent dans le bloc catch(){…}
est exécuté, mais le programme suit son cours !
Avant de voir comment créer nos propres exceptions, sachez que le bloc permettant de capturer ces dernières offre une fonctionnalité importante. En fait, vous avez sans doute compris que lorsqu'une ligne de code lève une exception, l'instruction dans le bloc try
est interrompue et le programme se rend dans le bloc catch
correspondant à l'exception levée.
Prenons un cas de figure très simple : imaginons que vous souhaitez effectuer une action, qu'une exception soit levée ou non (nous verrons lorsque nous travaillerons avec les fichiers qu'il faut systématiquement fermer ceux-ci). Java vous permet d'utiliser une clause via le mot clé finally
. Voyons ce que donne ce code :
1 2 3 4 5 6 7 8 9 10 | public static void main(String[] args){ try { System.out.println(" =>" + (1/0)); } catch (ClassCastException e) { e.printStackTrace(); } finally{ System.out.println("action faite systématiquement"); } } |
Lorsque vous l'exécutez, vous pouvez constater que, même si nous tentons d'intercepter une ArithmeticException
(celle-ci se déclenche lors d'un problème de cast), grâce à la clause finally
, un morceau de code est exécuté quoi qu'il arrive. Cela est surtout utilisé lorsque vous devez vous assurer d'avoir fermé un fichier, clos votre connexion à une base de données ou un socket (une connexion réseau). Maintenant que nous avons vu cela, nous pouvons aller un peu plus loin dans la gestion de nos exceptions.
Les exceptions personnalisées
Nous allons perfectionner un peu la gestion de nos objets Ville
et Capitale
. Je vous propose de mettre en œuvre une exception de notre cru afin d'interdire l'instanciation d'un objet Ville
ou Capitale
présentant un nombre négatif d'habitants.
La procédure pour faire ce tour de force est un peu particulière. En effet, nous devons :
- créer une classe héritant de la classe
Exception
:NombreHabitantException
(par convention, les exceptions ont un nom se terminant par « Exception ») ; - renvoyer l'exception levée à notre classe
NombreHabitantException
; - ensuite, gérer celle-ci dans notre classe
NombreHabitantException
.
Pour faire tout cela, je vais encore vous apprendre deux mots clés :
throws
: ce mot clé permet de signaler à la JVM qu'un morceau de code, une méthode, une classe… est potentiellement dangereux et qu'il faut utiliser un bloctry{…}catch{…}
. Il est suivi du nom de la classe qui va gérer l'exception.throw
: celui-ci permet tout simplement de lever une exception manuellement en instanciant un objet de typeException
(ou un objet hérité). Dans l'exemple de notreArithmeticException
, il y a quelque part dans les méandres de Java unthrow new ArithmeticException()
.
Pour mettre en pratique ce système, commençons par créer une classe qui va gérer nos exceptions. Celle-ci, je vous le rappelle, doit hériter d'Exception
:
1 2 3 4 5 | class NombreHabitantException extends Exception{ public NombreHabitantException(){ System.out.println("Vous essayez d'instancier une classe Ville avec un nombre d'habitants négatif !"); } } |
Reprenez votre projet avec vos classes Ville
et Capitale
et créez ensuite une classe NombreHabitantException
, comme je viens de le faire. Maintenant, c'est dans le constructeur de nos objets que nous allons ajouter une condition qui, si elle est remplie, lèvera une exception de type NombreHabitantException
. En gros, nous devons dire à notre constructeur de Ville
: « si l'utilisateur crée une instance de Ville
avec un nombre d'habitants négatif, créer un objet de type NombreHabitantException
».
Voilà à quoi ressemble le constructeur de notre objet Ville
à présent :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public Ville(String pNom, int pNbre, String pPays) throws NombreHabitantException { if(pNbre < 0) throw new NombreHabitantException(); else { nbreInstance++; nbreInstanceBis++; nomVille = pNom; nomPays = pPays; nbreHabitant = pNbre; this.setCategorie(); } } |
throws NombreHabitantException
nous indique que si une erreur est capturée, celle-ci sera traitée en tant qu'objet de la classe NombreHabitantException
, ce qui nous renseigne sur le type de l'erreur en question. Elle indique aussi à la JVM que le constructeur de notre objet Ville
est potentiellement dangereux et qu'il faudra gérer les exceptions possibles.
Si la condition if(nbre < 0)
est remplie, throw new NombreHabitantException();
instancie la classe NombreHabitantException
. Par conséquent, si un nombre d'habitants est négatif, l'exception est levée.
Maintenant que vous avez apporté cette petite modification, retournez dans votre classe main
, effacez son contenu, puis créez un objet Ville
de votre choix. Vous devez tomber sur une erreur persistante, comme à la figure suivante ; c'est tout à fait normal et dû à l'instruction throws
.
Cela signifie qu'à partir de maintenant, vu les changements dans le constructeur, il vous faudra gérer les exceptions qui pourraient survenir dans cette instruction avec un bloc try{…}catch{}
.
Ainsi, pour que l'erreur disparaisse, il nous faut entourer notre instanciation avec un bloc try{…}catch{…}
, comme à la figure suivante.
Vous pouvez constater que l'erreur a disparu, que notre code peut être compilé et qu'il s'exécute correctement.
Attention, il faut que vous soyez préparés à une chose : le code que j'ai utilisé fonctionne très bien, mais il y a un autre risque, l'instance de mon objet Ville
a été déclarée dans le bloc try{…}catch{…}
et cela peut causer beaucoup de problèmes.
Ce code :
1 2 3 4 5 6 7 8 | public static void main(String[] args) { try { Ville v = new Ville("Rennes", 12000, "France"); } catch (NombreHabitantException e) { } System.out.println(v.toString()); } |
… ne fonctionnera pas, tout simplement parce que la déclaration de l'objet Ville
est faite dans un sous-bloc d'instructions, celui du bloc try{…}
. Et rappelez-vous : une variable déclarée dans un bloc d'instructions n'existe que dans celui-ci ! Ici, la variable v
n'existe pas en dehors de l'instruction try{…}
. Pour pallier ce problème, il nous suffit de déclarer notre objet en dehors du bloc try{…}
et de l'instancier à l'intérieur :
1 2 3 4 5 6 7 8 9 | public static void main(String[] args) { Ville v = null; try { v = new Ville("Rennes", 12000, "France"); } catch (NombreHabitantException e) { } System.out.println(v.toString()); } |
Mais que se passera-t-il si nous déclarons une Ville
avec un nombre d'habitants négatif pour tester notre exception ? En remplaçant « 12000 » par « -12000 » dans l'instanciation de notre objet ? C'est simple : en plus d'une exception levée pour le nombre d'habitants négatif, vous obtiendrez aussi une NullPointerException
.
Voyons ce qu'il s'est passé :
- Nous avons bien déclaré notre objet en dehors du bloc d'instructions.
- Au moment d'instancier celui-ci, une exception est levée et l'instanciation échoue !
- La clause
catch{}
est exécutée : un objetNombreHabitantException
est instancié. - Lorsque nous arrivons sur l'instruction «
System.out.println(v.toString());
», notre objet estnull
! - Une
NullPointerException
est donc levée !
Ce qui signifie que si l'instanciation échoue dans notre bloc try{}
, le programme plante ! Pour résoudre ce problème, on peut utiliser une simple clause finally
avec, à l'intérieur, l'instanciation d'un objet Ville
par défaut si celui-ci est null
:
1 2 3 4 5 6 7 8 9 10 11 12 | public static void main(String[] args) { Ville v = null; try { v = new Ville("Rennes", 12000, "France"); } catch (NombreHabitantException e) { } finally{ if(v == null) v = new Ville(); } System.out.println(v.toString()); } |
Pas besoin de capturer une exception sur l'instanciation de notre objet ici : le code n'est considéré comme dangereux que sur le constructeur avec paramètres.
Maintenant que nous avons vu la création d'une exception, il serait de bon ton de pouvoir récolter plus de renseignements la concernant. Par exemple, il serait peut-être intéressant de réafficher le nombre d'habitants que l'objet a reçu. Pour ce faire, nous n'avons qu'à créer un deuxième constructeur dans notre classe NombreHabitantException
qui prend un nombre d'habitants en paramètre :
1 2 3 4 5 | public NombreHabitantException(int nbre) { System.out.println("Instanciation avec un nombre d'habitants négatif."); System.out.println("\t => " + nbre); } |
Il suffit maintenant de modifier le constructeur de la classe Ville
en conséquence :
1 2 3 4 5 6 7 8 9 10 | public Ville(String pNom, int pNbre, String pPays) throws NombreHabitantException { if(pNbre < 0) throw new NombreHabitantException(pNbre); else { //Le code est identique à précédemment } } |
Et si vous exécutez le même code que précédemment, vous pourrez voir le nouveau message de notre exception s'afficher.
Ce n'est pas mal, avouez-le ! Sachez également que l'objet passé en paramètre de la clause catch
a des méthodes héritées de la classe Exception
: vous pouvez les utiliser si vous le voulez et surtout, si vous en avez l'utilité. Nous utiliserons certaines de ces méthodes dans les prochains chapitres. Je vais vous faire peur : ici, nous avons capturé une exception, mais nous pouvons en capturer plusieurs !
La gestion de plusieurs exceptions
Bien entendu, ceci est valable pour toutes sortes d'exceptions, qu'elles soient personnalisées ou inhérentes à Java ! Supposons que nous voulons lever une exception si le nom de la ville fait moins de 3 caractères. Nous allons répéter les premières étapes vues précédemment, c'est-à-dire créer une classe NomVilleException
:
1 2 3 4 5 | public class NomVilleException extends Exception { public NomVilleException(String message){ super(message); } } |
Vous avez remarqué que nous avons utilisé super
? Avec cette redéfinition, nous pourrons afficher notre message d'erreur en utilisant la méthode getMessage()
.
Dans le code suivant, nous ajoutons une condition dans le constructeur Ville
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public Ville(String pNom, int pNbre, String pPays) throws NombreHabitantException, NomVilleException { if(pNbre < 0) throw new NombreHabitantException(pNbre); if(pNom.length() < 3) throw new NomVilleException("le nom de la ville est inférieur à 3 caractères ! nom = " + pNom); else { nbreInstance++; nbreInstanceBis++; nomVille = pNom; nomPays = pPays; nbreHabitant = pNbre; this.setCategorie(); } } |
Vous remarquez que les différentes erreurs dans l'instruction throws
sont séparées par une virgule. Nous sommes maintenant parés pour la capture de deux exceptions personnalisées. Regardez comment on gère deux exceptions sur une instruction :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | Ville v = null; try { v = new Ville("Re", 12000, "France"); } //Gestion de l'exception sur le nombre d'habitants catch (NombreHabitantException e) { e.printStackTrace(); } //Gestion de l'exception sur le nom de la ville catch(NomVilleException e2){ System.out.println(e2.getMessage()); } finally{ if(v == null) v = new Ville(); } System.out.println(v.toString()); |
Constatez qu'un deuxième bloc catch{}
s'est glissé… Eh bien, c'est comme cela que nous gérerons plusieurs exceptions !
Si vous mettez un nom de ville de moins de 3 caractères et un nombre d'habitants négatif, c'est l'exception du nombre d'habitants qui sera levée en premier, et pour cause : il s'agit de la première condition dans notre constructeur. Lorsque plusieurs exceptions sont gérées par une portion de code, pensez bien à mettre les blocs catch
dans un ordre pertinent.
Depuis Java 7 : le multi-catch
Encore une fois, Java 7 apporte une nouveauté : il est possible de catcher plusieurs exceptions dans l'instruction catch
. Ceci se fait grâce à l'opérateur « | » qui permet d'informer la JVM que le bloc de code est susceptible d'engendrer plusieurs types d'exception. C'est vraiment simple à utiliser et cela vous permet d'avoir un code plus compact. Voici à quoi ressemble l'exemple vu plus haut avec un catch multiple :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public static void main(String[] args){ Ville v = null; try { v = new Ville("Re", 12000, "France"); } //Gestion de plusieurs exceptions différentes catch (NombreHabitantException | NomVilleException e2){ System.out.println(e2.getMessage()); } finally{ if(v == null) v = new Ville(); } System.out.println(v.toString()); } |
Ce morceau de code nous remonte donc une erreur impliquant le nom de la ville (celui-ci fait moins de 3 caractères). Je vous invite à modifier le nombre d'habitants en le passant en négatif et vous verrez que l'exception concernant le nombre d'habitants est bien capturée.
- Lorsqu'un événement que la JVM ne sait pas gérer apparaît, une exception est levée (exemple : division par zéro). Une exception correspond donc à une erreur.
- La superclasse qui gère les exceptions s'appelle
Exception
. - Vous pouvez créer une classe d'exception personnalisée : faites-lui hériter de la classe
Exception
. - L'instruction qui permet de capturer des exceptions est le bloc
try{…}catch{}
. - Si une exception est levée dans le bloc
try
, les instructions figurant dans le bloccatch
seront exécutées pour autant que celui-ci capture la bonne exception levée. - Vous pouvez ajouter autant de blocs
catch
que vous le voulez à la suite d'un bloctry
, mais respectez l'ordre : du plus pertinent au moins pertinent. - Dans une classe objet, vous pouvez prévenir la JVM qu'une méthode est dite « à risque » grâce au mot clé
throws
. - Vous pouvez définir plusieurs risques d'exceptions sur une même méthode. Il suffit de séparer les déclarations par une virgule.
- Dans cette méthode, vous pouvez définir les conditions d'instanciation d'une exception et lancer cette dernière grâce au mot clé
throw
suivi de l'instanciation. - Une instanciation lancée par le biais de l'instruction
throw
doit être déclarée avecthrows
au préalable !