|
Les exceptions |
throw, try,
catch,...
La classe Throwable
Les exceptions Runtime
Les classes d'erreurs
Les autres exceptions
Pourquoi traiter des exceptions dès maintenant ? Parce qu'en Java, contrairement au C et au C++, les exceptions et leur traitement font partie intégrante du langage et sont utilisées systématiquement pour signaler toute erreur survenue pendant l'exécution d'une méthode (débordement d'indice d'un tableau, erreur d'accès à un fichier,...). De nombreuses méthodes sont susceptibles de déclencher (throw) des exceptions et donc il est impératif de savoir comment les traiter ou passer outre.
Une fois acquis le principe de cette forme de traitement d'erreur, vous pourrez utiliser les classes d'exceptions Java existantes ou créer vos propres classes pour traiter les erreurs qui peuvent survenir dans vos méthodes.La gestion d'erreur par exceptions permet d'écrire de manière plus claire (donc plus maintenable) un programme, en isolant le traitement d'erreur de la suite d'instructions qui est exécutée si aucune erreur ne survient. Généralement, dans les langages ne disposant pas des exceptions (comme le C), les fonctions susceptibles de poser problème renvoient des valeurs que vous devez traiter immédiatement pour vérifier si aucune erreur n'est survenue.
La gestion des erreurs se fait grâce aux exceptions en Java. Donc, il n'existe pas de variables telles que errno... Les exceptions sont d'un abord plus difficile mais une fois compris le principe, la programmation des erreurs se fait plus simplement et "proprement".
Les exceptions font partie du noyau du langage Java et leur gestion est obligatoire.
Syntaxe
En Java, cinq mots-clés servent à traiter les exceptions :
- L'instruction throw exception1; permet de déclencher l'exception exception1. exception1 doit être une référence sur un objet d'une classe d'exception. Toutes les classes d'exception sont de la classe Throwable ou de ses dérivées (les classes Error, Exception, RuntimeException et leur dérivées). Une exception peut être déclenchée par le système ou n'importe où dans une méthode.
- Le bloc suivant l'instruction try permet d'encadrer les séries d'instructions où une ou plusieurs exceptions sont susceptibles d'être déclenchées. Les instructions de ce bloc représentent le traitement normal de votre programme. Il peut donc comporter des instructions déclenchant ou non des exceptions et éventuellement des instructions throw.
- Le bloc de l'instruction try doit être suivi d'une ou plusieurs instructions catch, et chacun de ces catch doit être suivi d'un bloc d'instructions. Chaque catch spécifie quelle classe d'exception il est capable d'intercepter, quand une exception est déclenchée dans le bloc du try précédent le catch. Un catch capable de traiter la classe ClasseException ou une des classes dérivées de ClasseException respecte la syntaxe suivante :
try BlocInstructionsTry catch (ClasseException exceptionInterceptee) BlocInstructionsQuand une exception exception1 de classe ClasseException est déclenchée dans le bloc BlocInstructionsTry, le contrôle passe au premier catch suivant BlocInstructionsTry qui traite la classe d'exception ClasseException. Ce catch reçoit en paramètre l'exception déclenchée.
Si aucun de ces catch n'est capable d'intercepter exception1, le contrôle est rendu au premier catch capable d'intercepter une exception de classe ClasseException, parmi les méthodes mémorisées dans la pile d'exécution et exécutant un try ... catch. Si aucun catch n'est rencontré, la Machine Virtuelle Java indique l'exception qui est survenue et arrête le thread dans laquelle elle est survenue (ce qui généralement bloque le programme).
Le bloc instructions d'un catch peut éventuellement redéclencher l'exception interceptée exceptionInterceptee pour la propager dans la pile d'exécution, grâce à l'instruction throw exceptionInterceptee;.- Le bloc d'instructions du dernier catch peut être optionnellement suivi de l'instruction finally, suivi lui aussi d'un bloc d'instructions spécifiant les instructions qu'il faut toujours exécuter à la suite du bloc try si aucune exception n'a été déclenchée ou à la suite du traitement d'un catch.
- Dans la déclaration d'une méthode methode1 (), le mot-clé throws permet de déclarer la liste des classes d'exceptions que methode1 () est susceptible de déclencher. methode1 () peut déclencher des exceptions dans les deux situations suivantes :
- Elle appelle une ou plusieurs instructions throw exception1; et n'intercepte pas toutes ces exceptions avec l'instruction catch.
- Elle appelle d'autres méthodes susceptibles de déclencher des exceptions et n'intercepte pas toutes ces exceptions.
Seules les classes d'exceptions RuntimeException, Error et toutes leurs classes dérivées, ne sont pas obligées d'être citées après throws.
Ce type de déclaration permet d'être sûr qu'une exception déclenchée par une méthode sera toujours traitée ou ignorée sciemment par le programmeur.
Chacune des instructions try, catch et finally doivent être suivi d'un bloc { ... } même si ce bloc ne comporte qu'une seule d'instruction (Désolé, les afficionados du code compact en seront pour leurs frais !).
Voici un exemple de traitement local d'une exception déclenchée grâce à throw dans un bloc try et interceptée par un des catch qui suivent :
class Classe0 { // ... void methode1 () { try { // ... throw new Exception (); } catch (Exception exception1) { // Que faire en cas d'exception ? } } }Une méthode qui est susceptible de déclencher une exception peut s'écrire ainsi :
class Classe1 { // ... void methode1 () throws ClasseException { // ... // En cas d'erreur, déclenchement d'une exception throw new ClasseException (); // ... } }ClasseException est soit une classe prédéfinie dérivée de la classe Throwable, soit une classe dérivée d'une de ces classes créé par vous.
Quand vous appelez methode1 (), vous devez soit inclure l'appel à cette méthode dans un try ... catch, soit déclarer que la méthode qui appelle methode1 () est susceptible de déclencher une exception de la classe ClasseException, comme dans l'exemple suivant :
class Classe2 { Classe1 objet1 = new Classe1 (); // ... void methodeX () { try { objet1.methode1 (); // ... } catch (ClasseException exception1) { // Que faire en cas de problème ? } // ... Eventuellement d'autres catch (...) finally { // Le bloc finally est optionnel // Que faire après que le bloc try ou // qu'un bloc catch aient été exécutés ? } } void methodeY () throws ClasseException { objet1.methode1 (); // ... } }Le bloc finally est toujours exécuté, même si l'instruction return est exécutée dans les blocs try et catch : il sert à regrouper les instructions qu'il faut exécuter pour laisser dans un état correct votre programme qu'une exception est été déclenchée ou non. Par exemple, si le bloc try traite des accès à un fichier (ouverture, lecture/écriture,...), il est logique de fermer ce fichier dans le bloc finally, pour qu'il soit toujours finalement fermé.
Si une exception exception1 est déclenchée dans un bloc try et qu'aucun catch qui suit try n'intercepte exception1, alors le bloc finally est exécuté avant que le système ne continue à propager l'exception et trouve (ou non) un catch traitant les exceptions de la classe de exception1.
Si une exception exception2 est déclenchée dans un bloc catch, alors le bloc finally est aussi exécuté avant que le système ne continue à propager cette exception et trouve (ou non) un catch traitant les exceptions de la classe de exception2.
figure 9. Chemin parcouru lors du traitement d'exceptionsLa figure précédente illustre les chemins différents par lesquels peut passer le contrôle dans les méthodes methodeX () et methodeY (), suivant qu'une exception dans methode1 () est déclenchée (chemins vert et jaune) ou non (chemins rouge et bleu).
Afin de bien comprendre la gestion des erreurs avec les exceptions, voici un programme C typique traduit en Java où vous pourrez faire le parallèle entre constantes numériques façon C, et exception façon Java (à recopier dans un fichier EssaiException.java compilé avec l'instruction javac EssaiException.java, pour ensuite l'exécuter avec java ou Java Runner, grâce à l'instruction java EssaiException) :
/* Déclaration des constantes d'erreur */ #define ERR_OK 0 #define ERR_A_EGAL_B 1 int uneMethode (int a, int b) { if (a == b) return ERR_A_EGAL_B; else { printf ("%d et %d OK !\n", a, b); return ERR_OK; } } int main () { int erreur; if ((erreur = uneMethode (1, 2)) == ERR_OK) printf ("Pas d'erreur\n"); else if (erreur == ERR_A_EGAL_B) printf ("Erreur\n"); } // Déclaration d'une classe d'exception class AEgalBException extends Exception { public String toString () { return "A égal à B !"; } } public class EssaiException { static void uneMethode (int a, int b) throws AEgalBException { if (a == b) throw new AEgalBException (); else System.out.println (a + " et " + b + " OK !"); } public static void main (String [ ] args) { try { uneMethode (1, 2); System.out.println ("Pas d'erreur"); } catch (AEgalBException e) { System.out.println ("Erreur " + e); } } }
Voici un autre exemple d'application dont la méthode newObject () permet de créer un objet en connaissant le nom de sa classe. Cette méthode simplifie le traitement des exceptions que peuvent déclencher les méthodes forName () et newInstance () de la classe Class en renvoyant une exception de classe IllegalArgumentException qu'il n'est pas obligatoire d'intercepter (à recopier dans un fichier InstantiationAvecNom.java compilé avec l'instruction javac InstantiationAvecNom.java pour ensuite l'exécuter avec java ou Java Runner, grâce à l'instruction java InstantiationAvecNom) :
public class InstantiationAvecNom { // Methode transformant toutes les exceptions qui peuvent survenir // pendant l'instanciation d'une classe en une exception IllegalArgumentException. // nomClasse doit indiquer un nom de classe avec son package public static Object newObject (String nomClasse) { try { return Class.forName (nomClasse).newInstance (); } catch (ClassNotFoundException e) { throw new IllegalArgumentException ( "La classe " + nomClasse + " n'existe pas"); } catch (InstantiationException e) { throw new IllegalArgumentException ( "La classe " + nomClasse + " est abstract" + " ou n'a pas de constructeur accessible par d\u00e9faut"); } catch (IllegalAccessException e) { throw new IllegalArgumentException ( "La classe " + nomClasse + " n'est pas public"); } } public static void main (String [ ] args) { // Essai avec différentes classes String nomsClasses [] = {"java.lang.Object", // Ok "java.lang.String", // Ok "java.lang.Integer", // Pas de constructeur par défaut "java.lang.Runnable"}; // Interface (= classe abstract) for (int i = 0; i < nomsClasses.length; i++) try { System.out.println (nomsClasses [i] + " : " + newObject (nomsClasses [i])); } catch (IllegalArgumentException e) { System.out.println (e); System.out.println ("La classe " + nomsClasses [i] + " ne peut etre instancie par Class.forName (\"" + nomsClasses [i] + "\").newInstance ();"); } } }Autres exemples
Applets Chrono, ObservateurCalcul, EchoClient, PaperBoardClient, PlayApplet, CalculetteSimple, BoutonsNavigation, AnimationFleche, ScrollText et Horloge.
Applications LectureFichier, NumerotationLigne, ConcatenationFichiers, TestProtocole, HelloFromNet, EchoServer et PaperBoardServer.Avantages des exceptions
Bien que d'un abord plus compliqué qu'une gestion d'erreur avec des constantes numériques, les exceptions comportent de nombreux avantages que vous percevrez à l'usage :
- Chaque exception est une instance d'une classe : cette classe peut comporter toute sorte de méthodes et de champs, qui permettent une gestion d'erreur bien plus riche qu'une simple constante numérique. De plus, vous pouvez créer une hiérarchie de classes d'exceptions, si besoin est.
- Une méthode methode1 () est obligée de déclarer la liste des classes d'exceptions qu'elle est susceptible de déclencher, grâce à la clause throws. Ceci oblige les utilisateurs de methode1 () de prendre en compte ces exceptions, soit en les traitant dans un try ... catch (voir methodeX ()), soit en les ajoutant à la liste des classes d'exceptions déclenchées par leur méthode (voir methodeY ()). Cette obligation peut paraître lourde à priori, mais elle assure une gestion correcte des erreurs qui peuvent survenir dans un programme. (Qui peut affirmer qu'il a toujours géré toutes les erreurs dans un programme C ?...).
- Le bloc d'instructions d'un try représente la suite des instructions qui sont censées se dérouler s'il n'y a pas d'erreur. Quand vous retournez des codes d'erreurs, vous devez les tester tout de suite pour traiter les cas d'erreurs éventuelles : ceci peut nuire à la lisibilité du code.
- Quand une exception est déclenchée, le système recherche dans la pile d'exécution la première méthode qui traite cette exception dans un bloc catch. Comme dans l'exemple qui suit, ceci permet éventuellement de centraliser vos traitements d'exception dans une méthode methodePrincipale () au lieu de traiter toutes les exceptions qui peuvent survenir dans chacune des méthodes methodeI () où pourrait survenir une exception.
class UneClasse { private void methodeQuiDeclencheUneException () throws Exception { throw new Exception (); } private void methode1 () throws Exception { methodeQuiDeclencheUneException (); } private void methode2 () throws Exception { methode1 (); methodeQuiDeclencheUneException (); } private void methode3 () throws Exception { methode1 (); } public void methodePrincipale () { try { methode2 (); methode3 (); } catch (Exception exception) { // Que faire en cas d'exception } } }
L'équivalent de la clause catch (...) du C++ est catch (Throwable exception). En effet, toutes les classes d'exceptions Java héritent de la classe Throwable.
La clause throw; qui permet de redéclencher une exception traitée dans un catch, a pour équivalent en Java throw exception;, où exception est l'exception reçu en paramètre par le catch.
Java introduit le mot-clé throws qui permet de spécifier la liste des classes d'exceptions que peut déclencher une méthode, et que doit prendre en compte tout utilisateur de cette méthode (certains compilateurs C++ utilisent throw).
Le traitement des exceptions en Java comporte une clause supplémentaire et optionnelle par rapport au C++ : l'instruction finally. Cette instruction permet de spécifier l'ensemble des instructions à exécuter une fois terminé le bloc d'instructions d'un try ou d'un des catch, qu'une exception ait été déclenchée ou non.
Soit methode1 () une méthode d'une classe Classe1, déclarant avec la clause throws une liste d'exception ClasseExceptionI qu'elle est susceptible de déclencher. Si methode1 () est outrepassée dans une classe Classe2 dérivée de Classe1, alors cette méthode ne peut déclarer que les exceptions ClasseExceptionI ou les exceptions dérivées de ClasseExceptionI (interdiction de déclencher des exceptions dont les classes ne sont pas liées à celles que peut déclencher la méthode outrepassée).
Bien sûr, ceci ne s'applique que pour les classes d'exceptions différentes de RuntimeException, Error et toutes leurs classes dérivées.
Voici un exemple vous montrant ceci :abstract class Classe1 { abstract Class chercherClasse (String nomClasse); } class Classe2 extends Classe1 { // chercherClasse () est outrepassée Class chercherClasse (String nomClasse) { try { if (nomClasse.equals ("")) throw new IllegalArgumentException ("Nom de classe vide"); return Class.forName (nomClasse); } catch (ClassNotFoundException e) { // nomClasse pas trouvée : la méthode forName () de la classe // Class impose d'intercepter cette exception... throw new IllegalArgumentException ("Nom de classe inconnu"); } // IllegalArgumentException est une classe dérivée de // RuntimeException donc il n'est pas obligatoire d'intercepter // les exceptions de cette classe } // Vous auriez pu décider de ne pas intercepter l'exception de // de class ClassNotFoundException et de déclarer par exemple : /* Class chercherClasse (String nomClasse) throws ClassNotFoundException { return Class.forName (nomClasse); } */ // Ceci génère une erreur car il n'est pas possible d'outrepasser // chercherClasse () et de déclarer que cette méthode est susceptible de // déclencher des exceptions que chercherClasse () de Classe1 ne déclare pas... }Par contre, une méthode methode1 () outrepassant celle d'une super classe peut ne pas déclencher certaines des exceptions que la méthode outrepassée a déclarées dans sa clause throws, comme par exemple :
class Classe1 { void methode1 () throws Exception { // ... throw new Exception (); } } class Classe2 extends Classe1 { void methode1 () { // ... } }Ceci peut être utile quand vous voulez outrepasser la méthode clone () de la classe Object dans une classe Classe1 pour permettre dans certains cas, de cloner les objets de la classe Classe1 sans avoir à intercepter l'exception CloneNotSupportedException :
class Classe1 implements Cloneable { // ... // La méthode clone () de la classe Object peut déclencher // une exception CloneNotSupportedException, mais ici // dans Classe1 clone () ne le fait pas public Object clone () { try { Classe1 clone = (Classe1)super.clone (); // ... return clone; } catch (CloneNotSupportedException e) { // Ne peut survenir car cette classe implémente Cloneable // mais obligation d'intercepter l'exception car la méthode clone () // de la classe Object déclare qu'elle peut déclencher une exception // de classe CloneNotSupportedException return null; } } } class Classe2 { void methode (Classe1 objet1) { Classe1 objet2 = (Classe1)objet1.clone (); // ... } }
Les classes d'exceptions Java se divisent en plusieurs catégories. Elles héritent toutes de la classe Throwable décrite ci-dessous. Celle-ci n'est pas habituellement utilisée directement, mais toutes les exceptions héritent de ses méthodes, qui peuvent être intéressantes à utiliser ou à outrepasser.
Constructeurs
public Throwable () public Throwable (String message)Allocation d'un nouvel objet Throwable, l'un sans message et l'autre avec message décrivant l'exception survenue. Une trace de l'état de la pile est automatiquement sauvegardé.
Méthodes
public String getMessage ()Renvoie le message détaillé associé à l'objet.
public void printStackTrace () public void printStackTrace (PrintStream s)
public Throwable fillInStackTrace ()
Réinitialise la trace de la pile d'exécution. Cette méthode est utile uniquement quand vous voulez redéclencher une exception traitée par un catch, de la manière throw exception.fillInStackTrace ().
public String toString ()
Méthode outrepassée de la classe Object, renvoyant une description sommaire de l'exception
Les catégories des exceptions Runtime (classe java.lang.RuntimeException et ses dérivées) et des classes d'erreurs (classe java.lang.Error et ses dérivées) sont spéciales : contrairement aux autres classes d'exceptions, un programme n'est pas obligé de traiter toutes les instructions pouvant déclencher ce type d'exceptions dans un try ... catch, et ceci essentiellement pour des raisons pratiques de programmation. En effet, en consultant la liste suivante vous vous rendrez compte que ces exceptions peuvent survenir très souvent dans un programme : Si le compilateur Java obligeait à prévoir un traitement en cas d'exception à chaque fois qu'une instruction peut déclencher une exception Runtime, votre programme aurait beaucoup plus de traitement d'exceptions que de code réellement utile.
De même, si vous voulez vous servir d'une de ces classes pour déclencher avec throw une exception dans une méthode methode1 (), vous n'êtes pas obligé de la déclarer dans la clause throws de methode1 ().
La classe RuntimeException dérive de la classe Exception. Voici la liste des exceptions dérivant de RuntimeException, qui sont susceptibles d'être déclenchées au cours de l'exécution d'un programme Java :
Le package java.util définit les exceptions suivantes signalant des opérations interdites :
|
Il n'est pas obligatoire de traiter les exceptions des classes RuntimeException, Error et leur dérivées dans un try ... catch, et ceci qu'elles soient citées ou non dans la clause throws, des méthodes invoquées. En fait, quand ce type d'exception est cité, ce n'est que pour information. |
|
A la lecture de la liste précédente, vous pouvez voir
que la Machine Virtuelle Java gère de manière fine les
erreurs courantes qui peuvent survenir dans un programme. Au cours
de la mise au point d'un programme, ce type d'erreur survient souvent
: par défaut, l'exception déclenchée sera interceptée
par la Machine Virtuelle qui va inscrire à l'écran l'exception
en question ainsi que l'état de la pile d'exécution
au moment de son déclenchement. Grâce à ces informations,
vous retrouverez généralement très rapidement
d'où provient l'erreur sans avoir à lancer un debugger. |
Les classes d'erreurs dérivent de la classe Error, qui dérive elle-même de la classe Throwable. Elles sont généralement provoquée par la Machine Virtuelle Java à l'exécution, suite à un problème sur le chargement ou l'utilisation des classes, ou sur la Machine Virtuelle elle-même. Elles sont intéressantes à analyser quand elles sont déclenchées par un programme. Voici la liste de ces erreurs :
Vous devez obligatoirement prendre en compte les exceptions dont la classe dérive de java.lang.Exception (sauf RuntimeException et ses classes dérivées), soit en les traitant dans un try ... catch, soit, grâce à la clause throws, en les ajoutant à la liste des classes d'exception susceptibles d'être renvoyées par une méthode. La classe Exception dérive de la classe Throwable. Les classes d'exceptions qui suivent sont déclenchées par certaines méthodes de la bibliothèque Java :
Les packages java.io et java.net définissent les exceptions suivantes qui permettent de vérifier les erreurs d'entrées-sorties. Ces classes dérivent toutes de la classe java.io.IOException (qui dérive elle-même de la classe Exception) :
Le package java.awt définit l'exception suivante :
Pour le traitement d'erreur de vos programmes, vous pouvez déclencher vous-même des exceptions en utilisant les classes d'exceptions existantes (comme par exemple IllegalArgumentException) ou des nouvelles classes d'exceptions.
|