Programmation Java/Exceptions
Une exception est un signal qui se déclenche en cas de problème. Les exceptions permettent de gérer les cas d'erreur et de rétablir une situation stable (ce qui veut dire, dans certains cas, quitter l'application proprement). La gestion des exceptions se décompose en deux phases :
En Java, une exception est représentée par une classe.
Toutes les exceptions dérivent de la classe "Exception" qui dérive de la classe "Throwable".
Levée d'exception.
Une exception est levée grâce à l'instruction "throw" :
if (k<0)
 throw new Exception("k négatif");
Une exception peut être traitée directement par la méthode dans laquelle elle est levée dans un bloc codice_1, ou bien sans traitement dans la méthode, elle est envoyée à la méthode appelante auquel cas l'instruction "throws" (à ne pas confondre avec "throw") doit être employée pour indiquer les exceptions non traitées :
import java.io.IOException;
public void maMethode(int entier) throws IOException
 // code de la méthode
Dans cet exemple, si une exception de type "IOException" non traitée est levée durant l'exécution de "maMethode", l'exception sera envoyée à la méthode appelant "maMethode", qui devra la traiter.
Certaines exceptions sont levées implicitement par la machine virtuelle :
Celles-ci n'ont pas besoin d'être déclarées avec l'instruction "throws" car elles dérivent de la classe codice_5, une classe d'exceptions qui ne sont pas censées être lancées par une méthode codée et utilisée correctement.
Traitement d'exception.
Le traitement des exceptions se fait à l'aide de la séquence d'instructions "try...catch...finally".
Il faut au moins une instruction "catch" ou "finally" pour chaque instruction "try".
Exemple :
public String lire(String nomDeFichier) throws IOException
 try
 // La ligne suivante est susceptible de lever une exception
 // de type FileNoFoundException
 FileReader lecteur = new FileReader(nomDeFichier);
 char[] buf = new char[100];
 // Cette ligne est susceptible de lever une exception
 // de type IOException
 lecteur.read(buf,0,100);
 return new String(buf);
 catch (FileNotFoundException fnfe)
 fnfe.printStackTrace(); // Indique l'exception sur le flux d'erreur standard
 finally
 System.err.println("Fin de méthode");
Le bloc codice_6 capture toute exception du type codice_7 (cette classe dérive de la classe codice_8).
Le bloc codice_9 est exécuté quel que soit ce qui se passe (exception ou non).
Toute autre exception non capturée (telle codice_8) est transmise à la méthode appelante, et doit toujours être déclarée pour la méthode, en utilisant le mot clé codice_11, sauf les exceptions dérivant de la classe codice_5.
S'il n'y avait pas cette exception à la règle, il faudrait déclarer codice_13 chaque fois qu'une méthode utilise un tableau, ou codice_14 chaque fois qu'une expression est utilisée, par exemple.
Ne jamais ignorer une exception.
Il est tentant de vouloir ignorer une ou des exceptions en la capturant pour ne faire aucun traitement et en poursuivant l'exécution, comme dans cet exemple tiré des fichiers sources de Java :
 if (formatName != null) {
 try {
 Node root = inData.getAsTree(formatName);
 outData.mergeTree(formatName, root);
 } catch(IIOInvalidTreeException e) {
 // ignore
Cependant, c'est une très mauvaise pratique, car l'exception indique que les données d'entrée comportent une erreur mais l'utilisateur ou le développeur n'est pas informé. La poursuite de l'exécution aboutira alors à la production d'un résultat mauvais dont l'origine sera très difficile à remonter.
Il vaut mieux :
Remonter l'exception permet un traitement approprié (correction de code, message d'erreur à l'utilisateur pour qu'il corrige les valeurs d'entrées, essayer une autre solution, ...).
Classes et sous-classes d'exception.
L'héritage entre les classes d'exceptions peut conduire à des erreurs de programmation. En effet, une instance d'une sous-classe est également considérée comme une instance de la classe de base.
Ordre des blocs catch.
L'ordre des blocs codice_1 est important : il faut placer les sous-classes avant leur classe de base. Dans le cas contraire le compilateur génère l'erreur codice_16.
Exemple d'ordre incorrect :
try
 FileReader lecteur = new FileReader(nomDeFichier);
catch(IOException ioex) // capture IOException et ses sous-classes
 System.err.println("IOException capturée :");
 ioex.printStackTrace();
catch(FileNotFoundException fnfex) // <-- erreur ici
// FileNotFoundException déjà capturé par catch(IOException ioex)
 System.err.println("FileNotFoundException capturée :");
 fnfex.printStackTrace();
L'ordre correct est le suivant :
try
 FileReader lecteur = new FileReader(nomDeFichier);
catch(FileNotFoundException fnfex)
 System.err.println("FileNotFoundException capturée :");
 fnfex.printStackTrace();
catch(IOException ioex) // capture IOException et ses autres sous-classes
 System.err.println("IOException capturée :");
 ioex.printStackTrace();
Sous-classes et clause throws.
Une autre source de problèmes avec les sous-classes d'exception est la clause codice_11. Ce problème n'est pas détecté à la compilation.
Exemple :
public String lire(String nomDeFichier) throws FileNotFoundException
 try
 FileReader lecteur = new FileReader(nomDeFichier);
 char[] buf = new char[100];
 lecteur.read(buf,0,100);
 return new String(buf);
 catch (IOException ioe) // capture IOException et ses sous-classes
 ioe.printStackTrace();
Cette méthode ne lancera jamais d'exception de type codice_7 car cette sous-classe de codice_8 est déjà capturée.
Relancer une exception.
Une exception peut être partiellement traitée, puis relancée. On peut aussi relancer une exception d'un autre type, cette dernière ayant l'exception originale comme cause.
Dans le cas où l'exception est partiellement traitée avant propagation, la relancer consiste simplement à utiliser l'instruction codice_20 avec l'objet exception que l'on a capturé.
Exemple:
public String lire(String nomDeFichier) throws IOException
 try
 FileReader lecteur = new FileReader(nomDeFichier);
 char[] buf = new char[100];
 lecteur.read(buf,0,100);
 return new String(buf);
 catch (IOException ioException) // capture IOException et ses sous-classes
 // ... traitement partiel de l'exception ...
 throw ioException; //<-- relance l'exception
Une exception d'un autre type peut être levée, par exemple pour ne pas propager une exception de type SQLException à la couche métier, tout en continuant à arrêter l'exécution normale du programme :
 catch (SQLException sqlException) // capture SQLException et ses sous-classes
 throw new RuntimeException("Erreur (base de données)...", sqlException);
La pile d'appel est remplie au moment de la création de l'objet exception.
C'est à dire que les méthodes codice_21 affiche la localisation de la création de l'instance.
Pour mettre à jour la pile d'appel d'une exception pré-existante (réutilisation pour éviter une allocation mémoire, ou relancer une exception), la méthode codice_22 peut être utilisée :
 catch (IOException ioException) // capture IOException et ses sous-classes
 // ... traitement partiel de l'exception ...
 ioException.fillInStackTrace(); // <-- pile d'appel mise à jour pour pointer ici
 throw ioException; // <-- relance l'exception
Catégorie d'objet lancé.
Le chapitre traite des exceptions, mais en fait tout objet dont la classe est ou dérive de la classe codice_23 peut être utilisé avec les mots-clés codice_20, codice_11 et codice_1.
Classes dérivées de Throwable.
Il existe deux principales sous-classes de la classe codice_23 :
Créer une classe d'exception.
Il est également possible d'étendre une classe d'exception pour spécialiser un type d'erreur, ajouter une information dans l'objet exception, ...
Exemple :
public class HttpException extends Exception
 private int code;
 public HttpException(int code,String message)
 super(""+code+" "+message);
 this.code=code;
 public int getHttpCode()
Une instance de cette classe peut ensuite être lancée de la manière suivante :
public void download(URL url) throws HttpException
 ...
 throw new HttpException ( 404, "File not found" );
et capturée comme suit :
try
 download( ... );
catch(HttpException http_ex)
 System.err.println("Erreur "+http_ex.getHttpCode());
Fermeture des ressources.
Les objets de ressource alloue de la mémoire ou maintienne une ressource ouverte (un flux de fichier, une socket, ...).
Il est important d'assurer la libération de la mémoire ou la fermeture de la ressource, quoi qu'il se passe, exception ou non.
Dans le cas contraire, la ressource ne serait libérée qu'à la fin de l'application, qui peut donc cumuler les ressources ouvertes pouvant provoquer une fuite mémoire, une pénurie de ressource système, ...
Bloc finally.
La première solution pour libérer une ressource est de le faire dans un bloc codice_9.
Celui-ci doit être précédé du bloc codice_31 contenant le code utilisant la ressource ouverte.
FileInputStream in = new FileInputStream(new File("/tmp/config.ini"));
// la ligne précédente peut échouer et lancer une exception,
// mais rien à fermer dans ce cas car le flux n'a pas pu être ouvert.
// Donc le bloc try commence juste après :
try
 // ... lecture du fichier en utilisant in pour lire les données ...
finally
 // Quoi qu'il se passe, fermeture après utilisation :
 in.close();
Fermeture automatique.
La seconde solution s'applique aux classes implémentant l'interface codice_32.
La déclaration des instances de ces classes peut être employée en paramètre du mot-clé codice_31.
try(FileInputStream in = new FileInputStream(new File("/tmp/config.ini")))
 // ... lecture du fichier en utilisant in pour lire les données ...
// Quoi qu'il se passe, la fermeture après utilisation
// est effectuée implicitement à la fin du bloc try.
Ce code est équivalent à l'exemple de la section précédente, en moins de lignes de code.
Le bloc codice_31 pour cette syntaxe peut accepter des blocs codice_1 pour traiter des exceptions ou codice_9 pour libérer des ressources, mais sont optionnels.
Exceptions non capturées.
Une exception lancée explicitement ou implicitement depuis une méthode peut ne pas être capturée.
Dans ce cas, elle est remontée au niveau de la méthode appelante.
La classe de cette exception (ou une classe parente) doit être déclarée dans une clause codice_11 à la fin de la déclaration de la méthode appelée, à moins que la classe soit une sous-classe de codice_38 ou codice_39.
La méthode codice_40 peut aussi déclarer des exceptions.
Quand la méthode codice_40 ne capture pas une exception, celle-ci remonte à l'appelant (code interne de la JVM) qui affiche l'exception dans la console (flux d'erreur standard), et provoque donc l'arrêt de l'application si aucun thread non démon ne tourne.
Pour un thread, la méthode codice_42 des implémentations de l'interface codice_43 ne peut pas déclarer d'exceptions lancées.
Cela ne l'empêche pas de remonter les exceptions non déclarées (codice_38, codice_39, et leurs sous-classes).
Dans ce cas, le thread est interrompu et la JVM affiche l'exception dans la console (flux d'erreur standard).
Il est possible de traiter les exceptions non capturées par un thread.
L'interface codice_46 définie dans la classe codice_47 a une méthode codice_48 notifiée lorsqu'un thread n'a pas capturé une exception.
Exemple d'implémentation :
UncaughtExceptionHandler eh = new UncaughtExceptionHandler()
 @Override
 public void uncaughtException(Thread t, Throwable e)
 System.out.println("Une exception "+e);
 System.out.println("a eu lieu dans le thread "+t);
};
Ce gestionnaire peut ensuite être assigné à différents niveaux :
ThreadGroup tg_telechargeurs = new ThreadGroup()
 @Override
 public void uncaughtException(Thread t, Throwable e)
 System.out.println("Une exception "+e);
 System.out.println("a eu lieu dans le thread "+t);
};
Thread telecharge_html = new Thread(tg_telechargeurs, run_get_html);
Thread telecharge_css = new Thread(tg_telechargeurs, run_get_css);
Thread telecharge_js = new Thread(tg_telechargeurs, run_get_js);