Gestion des exceptions

Olivier Capuozzo

Ce document est placé sous la Licence GNU Free Documentation License, Version 1.1 ou ultérieure publiée par la Free Software Foundation. 

30 mars 2006

Résumé

Avec la séquence, l'alternative et l'itération, la gestion des exceptions fait pleinement partie de la gestion de base des structures de contrôle du flux d'exécution.

Le développeur "utilisateur"

  • Comprend les spécifications (API)

  • Connaît les alternatives de gestion

    1. Laisser passer l'exception (par spécification explicite ou non)

    2. Intercepter définitivement en réglant le problème (sans propagation)

    3. Intercepter et traduire dans une autre exception (avec spécification)

Le développeur "concepteur"

  • Gère les exceptions des composants qu'il utilise

  • Transforme les exceptions des composants en Exception Métier. Ne laisse jamais une exception technique remonter jusqu'à l'utilisateur.

Les langages de programmation propose des mécanismes obligeant ou non le développeur à prendre ses responsabilités vis à vis de certaines exceptions. Les exceptions contrôlées de Java doivent bligatoirement être gérées, les "Runtime Exceptions" sont elles gérées à la demande.

Une version simplifiée : gestionException.odt

Ce document a été réalisé sous GNU/Linux avec gvim, au format docbook , mis en page avec le processeur XSLT saxon développé par Michael Kay, les feuilles de styles de Norman Walsh.


Table des matières

1. Introduction
1.1. Exemple
2. Notion de contrat
3. Quand le contrat est rompu
3.1. Comment ça marche ?
3.2. Exemple
3.3. Forme générale d'une gestion d'exceptions en Java
3.4. Exercices
4. Stratégies de gestion des exceptions
4.1. Introduction
4.2. Propager l'erreur en organisant la panique
4.3. Régler l'erreur en appliquant une nouvelle stratégie
5. Comment faire avec Java ?
5.1. Comment provoquer une exception ?
5.2. Comment intercepter une exception ?
5.3. Comment provoquer l'évaluation d'une instruction coûte que coûte ?
6. Solutions des exercices 3.4
7. Exercice
8. Concusion
9. Références

1. Introduction

Une exception est un événement déclenché par une erreur survenant à l'exécution.

Une exception est la manifestation d'un bogue, le signe qu'une opération s'est interrompue anormalement.

Principe de programmation

[Note]Pas de retour pour les bogues

Une opération qui ne peut pas réaliser sa fonction ne doit pas retourner à l'appelant.

Les langages d'aujourd'hui fournissent un moyen d'adhérer à ce princpe, en fournissant un canal de retour dédié aux exceptions. L'erreur est alors canalisée, puis traitée avec plus ou moins de bonheur.

[Note]Règles de programmation
  • R1 : Une fonction (appelée - le fournisseur -) qui ne peut réaliser son contrat ne doit pas retourner à l'appelant, mais utiliser pour cela le canal des exceptions offerts par les langages de programmation.

  • R2 : La fonction appelante (le client) ne doit pas appeler une fonction si elle n'en remplit par les conditions d'utilisation (pré-conditions).

1.1. Exemple

Nous souhaitons, dans un formulaire en cours d'exécution, pouvoir changer l'image du produit courant. Pour cela nous créons une méthode qui reçoit le nom d'une image (un nom de fichier) et qui place celle-ci dans un label.

class Formulaire extends JFrame {
  private
     JLabel lbnomProduit;
        
  public void changeImage(String nomFicImage){
    ImageIcon icon = null;
    java.net.URL iconURL = ClassLoader.getSystemResource(nomFicImage);
    icon = new ImageIcon(iconURL);
    lbnomProduit.setIcon(icon);
  }   
  . . .
}  
  

Isolons une instruction :

Figure 1. Création d'un objet ImageIcon

Création d'un objet ImageIcon

Notre méthode changeImage a un objectif bien précis : placer une image dans un label. Pour réaliser cette tâche, elle délègue à un objet de la classe ImageIcon le soin de charger l'image en mémoire.

A cet instant précis, la méthode changeImage agit en tant que client et l'objet de type ImageIcon en tant que fournisseur de service. Situation rien de plus banale.

Comme dans toute relation client-fournisseur, il existe un contrat entre les parties. Dans une telle relation, chacune des parties assume ses obligations et peut faire valoir ses droits.

2. Notion de contrat

Revenons sur l'analogie client-fournisseur. Un contrat lie client et fournisseur.

Exemples de contrat affiché dans les bureaux de poste des Pays-Bas (source Warmer et Kleppe) :

  • Une lettre postée avant 18:00 sera délivrée le lendemain à n'importe qu'elle adresse dans le pays.

Pour un envoi express :

  • Pour le prix de cinq guilders, toute lettre postée d'un poids maximum de 80g sera délivrée n'importe où dans le pays dans les quatre heures qui suivent le relevé.

Nous pouvons logiquement en conclure qu'un client ayant posté une lettre d'un poids au plus égal à 80g puisse compter que cette lettre sera délivrée dans les condidtions du contrat (devoir du fournisseur), à condition qu'il ait payé (au moins ?) la somme attendue (devoir du client).

Bien sur, les contrats peuvent être bien plus complexes : contrat de travail, contrat de mariage, contrat de construction d'une maison etc.

[Note]Définition d'un contrat

Le principe de conception par contrat est dérivé de la notion légale de contrat : chacune des parties qui respecte ses obligations est en droit de faire valoir ses droits.

En terme de programmation, un objet est responsable de l'exécution des services dont il a la charge (ses obligations) si et seulement si certaines conditions sont remplies (ses droits).

Les obligations et droits sont représentés en UML par des pré et post conditions.

Les définitions suivantes, sont extraites du livre "Conception et programation orientées objet" de Bertrand Meyer (p. 335).

La précondition lie le client. Elle défine les conditions selon lesquelles un appel de méthode est légitime. C'est une obligation pour le client et un droit pour le fournisseur.

La postcondition définit les conditions qui doivent être vérifiées par la méthode au retour. C'est un droit pour le client et une obligation pour le fournisseur.

Contrairement à ce qui se pratique parfois, le fournisseur définit seul les termes du contrat, pas de négociation possible !

Tableau 1. Exemple de contrat relatif au constructeur ImageIcon de notre exemple.

 OBLIGATIONSDROITS
Client(pré-condition)

Fournir un chemin valide correspondant à un fichier image

obtenir une référence valide à un objet ImageIcon

Fournisseur(post-condition)

ouverture/lecture/fermeture du fichier et chargement de l'image en mémoire après décompression éventuelle.

traitement plus simple du fait de l'hypothèse que le chemin est valide.

3. Quand le contrat est rompu

Un contrat rompu est synonyme de bogue : l'une des deux parties n'a pas respecté son contrat.

Dans le but de répondre à cette problématique, certains langages de programmation proposent un mécanisme de gestion des contrats.

Toute fois, dans la majorité des cas, les spécifications des contrats (pré et post conditions) ne sont présentes que sous la forme de commentaires dans la documentation (documentation de l'API).

Les langages modernes (dont Java fait partie) proposent un mécanisme appelé gestion des exceptions.

[Note]Définition : exception

Exception : impossibilité pour une méthode de réaliser son contrat [1].

3.1. Comment ça marche ?

Lorsqu'une exception est déclenchée, un saut brutal arrière est effectué vers le premier gestionnaire try/catch intéressé par l'exception en remontant la chaîne des appels. Ainsi si aucun gestionnaire try/catch ne filtre l'exception en train de remonter, elle atteindra la fonction d'entrée du programme (le "main") arrêtant du même coup celui-ci.

3.2. Exemple

D'après : http://java.sun.com/docs/books/tutorial/essential/exceptions/index.html

L'exemple suivant définit et implémente une classe nommée ListOfCA.

Lors de la création d'un objet de cette classe, une liste de CA (chiffre d'affaire) est fournie (lesCA) ainsi que le nombre de valeurs (n) concernées par le traitement. Bien entendu, ce nombre ne devra pas être supérieur au nombre de valeurs détenues par la liste.

// Note: cette classe ne peut être compilée !

import java.io.*;
import java.util.Vector;

public class ListOfCA {
  private Vector lesCA;
  private int nbCA;
    
  public ListOfCA(Vector lesCA, int n) {
    this.lesCA = lesCA;
    this.nbCA = n;
  }
  public void enregistrerCA() {
    PrintWriter out = new PrintWriter(new FileWriter("lesCA.txt"));
       
    for (int i = 0; i < nbCA; i++)
      out.println("Valeur à l'indice : " + i + " = " + lesCA.elementAt(i));

      out.close();
  }
}

ListOfCA détient un objet Vector qui contient des valeurs de chiffres d'affaire (CA). La classe ListOfCA définit une méthode nommée enregistrerCA qui écrit la liste de chiffres d'affaire dans un fichier texte appelé lesCA.txt.

La méthode enregistrerCA appelle deux méthodes qui peuvent déclencher des exceptions.

Premièrement, la ligne suivante invoque le constructeur FileWriter, susceptible de déclencher (throw) une IOException, si le fichier ne peut être ouvert pour une raison quelconque :

  out = new PrintWriter(new FileWriter("lesCA.txt"));

Deuxièmement, la méthode elementAt de la classe Vector peut déclencher une exception de type ArrayIndexOutOfBoundsException si vous lui passez un index ayant une valeur trop petite (nombre négatif) ou trop grande (supérieure ou égale au nombre déléments contenus dans le vecteur).

  out.println("Valeur à l'indice : " + i + " = " + lesCA.elementAt(i));

Si vous essayer de compiler la classe ListOfCA, le compilateur affichera un message à propos de l'exception pouvant être déclenchée par le construteur FileWriter, mais ne dit rien au sujet de l'exception pouvant être déclenchée par elementAt. Ceci parce que l'exception (de type IOException déclenchée par le constructeur FileWriter est une exception contrôlée alors que l'exception ArrayIndexOutOfBoundsException déclenchée par elementAt est une runtime exception.

Java impose que seules les exceptions contrôlées soient interceptées (catch) ou spécifiées (throws).

Les RuntimeException sont des exceptions internes à la JVM pouvant être déclenchées dans de multiples contextes. Ces exceptions comprennent les exceptions arithmetiques (par exemple la division par zéro), les exceptions de pointeur (lors d'une tentative d'access à un objet par l'intermédiaire d'une référence null), les exceptions d'indexation (valeur d'index hors bornes).

Les Error sont des exceptions liées aux problèmes de chargement des classes en mémoire, problème matériel...

Figure 2. Extrait du graphe d'héritage des classes d'exception.

Extrait du graphe d'héritage des classes d'exception.

3.3. Forme générale d'une gestion d'exceptions en Java

Forme générale en java :

 try  <bloc d'instructions> 
 catch  <classe_d_une_exception variable> : <bloc d'instructions> 
 ...
 catch  <classe_d_une_exception variable> : <bloc d'instructions>
 finally <bloc d'instructions>
      

Le bloc d'instructions de try est sous surveillance d'exceptions contrôlées par les catch.

Un catch filtre une classe d'exception - pouvant intervenir dans le bloc sous la responsabilité du try.

Le bloc d'instructions de finally est un bloc dont l'exécution est garantie par le langage, qu'une exception intervienne (contrôlée ou non) ou non. A noter que ce bloc est un bloc comme un autre pouvant contenir également un gestionnaire d'exceptions... et donc être interruptible.

Exemple

public void enregistrerCA() {
  PrintWriter out = null;

  try {
     System.out.println("Entrée dans le bloc try");
     out = new PrintWriter(
               new FileWriter("lesCA.txt"));

     for (int i = 0; i < nbCA; i++)
       out.println("Valeur à l'indice : " + i + " = " + lesCA.elementAt(i));

     System.out.println("Fin de l'enregistrement.");    
  } catch (ArrayIndexOutOfBoundsException e) {
     System.err.println("Interception de ArrayIndexOutOfBoundsException: " +
                         e.getMessage());
  } catch (IOException e) {
     System.err.println("Interception de IOException: " + e.getMessage());
  } finally {
     if (out != null) {
       System.out.println("Fermeture de PrintWriter");
       out.close();
     } else {
       System.out.println("PrintWriter non ouvert");
     }
  }
}        

Le bloc sous contrôle de la clause finally sera toujours exécuté, qu'une exception intervienne ou pas.

3.4. Exercices

  1. Déterminer tous les cas possibles de sortie (sur le canal de sortie standard et d'erreur) de la méthode ci-dessus. Vous indiquerez également, selon le cas, le type de retour (normal ou sur le canal des exceptions).

    Une solution ici Réponse 1

  2. Voici une nouvelle version de la méthode, déterminer de nouveau tous les cas possibles de sortie sur le canal de sortie standard et d'erreur.

     public void enregistrerCA() throws IOException {
      PrintWriter out = null;
    
     try {
        System.out.println("Entrée dans le bloc try");
        out = new PrintWriter(
                  new FileWriter("lesCA.txt"));
    
        for (int i = 0; i < nbCA; i++)
          out.println("Valeur à l'indice : " + i + " = " + lesCA.elementAt(i));
    
        System.out.println("Fin de l'enregistrement.");    
     } catch (ArrayIndexOutOfBoundsException e) {
        System.err.println("Interception de ArrayIndexOutOfBoundsException: " +
                            e.getMessage());
     } finally {
        if (out != null) {
          System.out.println("Fermeture de PrintWriter");
          out.close();
        } else {
          System.out.println("PrintWriter non ouvert");
        }
     }
    }        
    

    Une solution ici Réponse 2

4. Stratégies de gestion des exceptions

4.1. Introduction

Que faire lorsqu'une erreur est signalée ?

Il existe 2 réponses légitimes à une rupture annoncée d'un contrat (Bertrand Meyer):

  • Propager l'erreur, c'est à dire demander à l'appelant de prendre ses responsabilités face à cette erreur.

  • Régler l'erreur en appliquant un autre algorithme (une autre stratégie).

Bertrand Meyer nomme ces deux[2] types de réponses à une exception respectivement panique organisée et réexécution. Nous illustrons chacune d'elles en reprenant l'exemple de la méthode changeImage. Cette méthode est chargée de changer l'image portée par le label lbnomProduit de la classe Formulaire (voir exemple Formulaire).

4.2. Propager l'erreur en organisant la panique

Abandonne le traitement (la stratégie courante) par le déclenchement d'une exception, en ayant, auparavant, pris soin de laisser l'objet courant dans un état cohérant.

Exemple : la ressource ne peut être atteinte.

  public void changeImage(String nomFicImage) 
  throws ImageIntrouvableException {
    java.net.URL iconURL = ClassLoader.getSystemResource(nomFicImage);
    
    if (iconURL==null) {
       lbnomProduit.setIcon(null);
       throw new ImageIntrouvableException(nomFicImage);
    }
    
    ImageIcon icon = new ImageIcon(iconURL);
    lbnomProduit.setIcon(icon);
    
  }   

La méthode déclare qu'elle est déclenchera une exception si elle ne trouve pas la ressource (voir la clause throws).

Conséquence : charge à l'appelant de régler le problème.

[Note]Sommes nous obligés de déclarer une exception personnalisée ?

Non. Nous aurions pu ne pas gérer le problème et laisser se propager une runtime exception, ici java.lang.NullPointerException. En agissant ainsi nous n'organisons en rien la panique ! et violons au moins les deux principes suivants (source Graig Larman) :

  • Convertir les exceptions de bas niveau en exceptions ayant un sens dans le domaine d'application.

  • Nommer le problème et non le déclencheur de l'exception.

4.3. Régler l'erreur en appliquant une nouvelle stratégie

Tenter une nouvelle stratégie pour satisfaire le contrat.

Exemple : la ressource est absente. La fonction subtitue une image de remplacement.

import javax.swing.*;  
class Formulaire extends JFrame {
  private
     JLabel lbnomProduit;
  static private
     ImageIcon iconInconnu = 
       new ImageIcon(ClassLoader.getSystemResource("inconnu.png"));
     // le fichier "inconnu.png" est disponible
     // dans le repertoire courant.

        
  public void changeImage(String nomFicImage){
    java.net.URL iconURL = ClassLoader.getSystemResource(nomFicImage);
    if (iconURL==null) {
       lbnomProduit.setIcon(iconInconnu);
    }
    else {
       ImageIcon icon = new ImageIcon(iconURL);
       lbnomProduit.setIcon(icon);
    }   
  }   
  . . .
}  

Nous pourrions aussi rechercher l'image sur un autre serveur, dans une base de données...

Conséquence : l'appelé prend en charge la résolution du problème.

[Note]L'appelé règle le problème

L'erreur potentielle (impossibilité de charger la ressource indiquée) fait l'objet d'un cas particulier programmé par le client (la méthode appelée). Une décision à ne pas prendre à la légère, qui doit faire l'objet d'une règle de gestion (une règle métier, un contrat).

Exemple : Si l'image ne peut être chargée en mémoire, on lui substitue une autre image type, porteuse d'un message bien clair (point d'interrogation, mot "inconnu" etc.).

5. Comment faire avec Java ?

5.1. Comment provoquer une exception ?

On utilise le mot clé throw pour lancer une exception et throws pour le spécifier à l'appelant le cas échéant.

Exemple, la méthode connexionBaseDeDonnées ne peut assurer sa fonction si le serveur de base de données ne répond pas.

Elle prévient alors l'appelant qu'elle est susceptible de générer une exception par la clause throws :

public void connexionBaseDeDonnées(...) 
  throws ConnexionImpossibleException  

... l'exception sera générée soit par un composant interne utilisé par la méthode, soit directement par la méthode, en créant un objet d'une classe d'Exception, et en le propageant par throw . Exemple :

public void connexionBaseDeDonnées(...) 
throws ConnexionImpossibleException {
  if (serveur.noResponding()) { 
    throw new ConnexionImpossibleException(); 
  }
  ...
} 

Charge alors à l'appelant d' intercepter ( catch ) l'exception.

Attention, il y a deux mots clés ici : throw et throws (avec un ' s ').

5.2. Comment intercepter une exception ?

L'appelant intercepte une exception en plaçant le code à risque dans le corps d'un try , puis en filtrant les exceptions selon leur classe, par un catch . Exemple :

... 
try {
  db.connexionBaseDeDonnées();
  String sql ="Select * from clients";
  db.excuteSql(sql);
  ...
 }
 cath (ConnexionImpossibleException ex) {
    // faire quelque chose ici
 } 

... 

[Note]Prise de responsabilités

Java impose que les Exceptions Contrôlées soient interceptées ( catch ) ou spécifiées ( throws ). Voir hiérarchie de classes d'exception.

En d'autres termes, le langage oblige le développeur à prendre explicitement ses responsabilités en ce qui concerne les Exceptions Contrôlées.

5.3. Comment provoquer l'évaluation d'une instruction coûte que coûte ?

La clause finally est là pour ça, à placer après le corps du try (comme les catch ). Exemple :


..
 try {
   db.connectionBaseDeDonnées();
   Strin sql ="Select * from clients";
   db.excuteSql(sql);
   ...
 }
 cath (ConnectionImpossibleException ex) {
    // faire quelque chose ici
 }
 finally {
   afficher ("Merci d'être venu.");
 }
...

Le message Merci d'être venu. sera toujours affiché, à moins que la méthode afficher ne soit à l'origine d'une exception.

6. Solutions des exercices 3.4

Figure 3. Scénarios possibles de la question 1

Scénarios possibles de la question 1

Figure 4. Scénarios possibles de la question 2

Scénarios possibles de la question 2

7. Exercice

Voici un programme Java qui demande l'année de naissance de l'utilisateur, et retourne son age en retour (à 12 mois près...)


import java.util.*;
import javax.swing.*;

class TestGE {

  public void run() {
    String input = 
       JOptionPane.showInputDialog("Entrez votre année de naissance");
    String age = calculerAge(input);
    JOptionPane.showMessageDialog(null, "Vous avez "+age+" ans.");
  }

  public String calculerAge(String annee) {
    String age;
    Calendar now = Calendar.getInstance();
    int ann_nais = Integer.parseInt(annee);
    age = String.valueOf(now.get(Calendar.YEAR)-ann_nais);
    return age;
  }

  static public void main(String[] args) {
     TestGE test = new TestGE();
     test.run();
     System.exit(0);
  }

}//TestGE

Ce programme manque sérieusement de robustesse. Encore une fois les données situées à l'extérieur en sont la cause. Est-ce bien raisonnable de faire confiance à l'utilisateur ? Que se passe-t-il s'il rentre mille neuf cent quatre vingt quatre au lieu de 1984 ?

On vous demande donc d'améliorer la robustesse de l'application.

Voici l'exception concernée :

Figure 5. "Classe Integer"

"Classe Integer"

8. Concusion

Face à l'éventualité d'un déclenchement d'exception, le développeur a le choix entre :

  • Ne pas traiter le problème. en renvoyant la responsabilité à l'appelant (propager l'exception)

  • Traiter le problème localement, sans en avertir l'appelant.

  • Traiter en partie le problème localement et propager une exception métier (à l'appelant donc).

Les instructions types de contrôle des exceptions (try, catch, finally, throw(s)), leurs logiques de mise en oeuvre, devrait faire partie du socle de base de l'enseignement des fondamentaux de tout informaticien.

9. Références

  • Bertrand Meyer : "Conception et programation orientées objet" ed. Eyrolles - 2000.

  • Graig Larman : "UML et les Design Patterns" ed. CampusPress - 2002.

  • Jos Warmer et Anneke Kleppe : "Precise Modeling with UML" ed. Addison-Wesley - 1999.



[1] ... via l'une de ses stratégies possibles. Peut provenir, en particulier, de l'échec d'une routine appelée (d'après Bertrand Meyer).

[2] plus une troisième Fausse Alarme : reprendre l'exécution de la routine en cours... Un cas rare non présenté ici, voir Bertrand Meyer.