Les exceptions

Ce contenu est obsolète. Il peut contenir des informations intéressantes mais soyez prudent avec celles-ci.

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{...}

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.

ArithmeticException

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.

Capture d'exception

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 type ArithmeticException 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 :

  1. créer une classe héritant de la classe Exception : NombreHabitantException (par convention, les exceptions ont un nom se terminant par « Exception ») ;
  2. renvoyer l'exception levée à notre classe NombreHabitantException ;
  3. 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 bloc try{…}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 type Exception (ou un objet hérité). Dans l'exemple de notre ArithmeticException, il y a quelque part dans les méandres de Java un throw 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.

Exception non gérée

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.

Correction du bug

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 objet NombreHabitantException est instancié.
  • Lorsque nous arrivons sur l'instruction « System.out.println(v.toString()); », notre objet est null !
  • 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. :magicien:


  • 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 bloc catch 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 bloc try, 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 avec throws au préalable !