Introduction à Java - les exceptions - Frédéric VOISIN FIIFO - « Remise à Niveau »
Pourquoi des « exceptions » ? Gérer des « situations exceptionnelles » Cas d’erreurs liées à l’environnement (JVM) à des erreurs de programmation (référence nulle, index de tableaux) à une application/classe particulière (next() sur un itérateur épuisé) … ou tout autre événement impliquant une rupture (partielle) du déroulement normal du programme Mécanisme utile si on ne sait pas que faire à l’endroit où le problème est découvert (on ne peut que le signaler à l’appelant, et autrement que textuellement !) Si on ne sait pas forcément à quel niveau le problème pourra être traité (par défaut on doit propager l’alerte)
Pourquoi des « exceptions » ? (suite) Signaler explicitement des cas « exceptionnels » Eviter de procéder par « retour de valeur particulière » qui ne sont pas toujours testées par l’appelant Eviter que les cas « erronés » soient ignorés : une exception non traitée interrompt le programme Eviter d’obscurcir les cas « normaux » avec les situations « exceptionnelles » (tester toute référence à null avant usage ? Conversions à partir de Object ? ) Autoriser des traitements de rattrapage arbitrairement complexes Autoriser des traitements sélectifs : chaque méthode décide quels cas exceptionnels elle sait traiter et quels cas relèvent des méthodes appelantes.
« Gérer » une exception ? Signaler une situation exceptionnelle (« lever une exception ») throw instance d’exception ; Se protéger vis-à-vis d’une situation exceptionnelle, si on est capable de la prendre en compte (rattraper une exception) ! try { liste d’instructions } // bloc protégé ! catch (ClasseException1 variable){ traitement correctif 1 } catch (ClasseException2 variable){ traitement correctif 2 } … Propager (explicitement ou implicitement) une exception qu’on ne sait pas traiter complètement Souvent on est amené à combiner traitement local + propagation à l’appelant (sous le même nom d’exception ou un autre !)
Qu’est-ce qu’une (classe) d’exception ? Une exception = une instance d’une classe membre d’une hiérarchie (extensible) de classes, de racine Throwable Error, Exception, RunTimeException, IOException, … Une classe d’exception est une classe « ordinaire », avec éventuellement des constructeurs, des attributs, des méthodes Méthodes héritées de Throwable : getMessage() toString() printStackTrace() // historique de propagation fillInStackTrace() + une description du contexte d’exécution au moment de la levée de l’exception Les règles de typage classiques (classe/sous-classes) s’appliquent ! On peut donc organiser ses exceptions en hiérarchie
class java.lang.Throwable sous-classe directe de Object ! class java.lang.Error Difficilement rattrapables ! class java.lang.VirtualMachineError class java.lang.OutOfMemoryError … class java.lang.Exception class java.lang.RuntimeException trop fréquentes pour qu’on oblige class java.lang.ArithmeticException à les signaler à l’avance ! class java.lang.ClassCastException elles peuvent donc survenir partout… class java.lang.IllegalArgumentException class java.lang.IllegalThreadStateException class java.lang.NumberFormatException class java.lang.IndexOutOfBoundsException class java.lang.ArrayIndexOutOfBoundsException class java.lang.StringIndexOutOfBoundsException class java.lang.NegativeArraySizeException class java.lang.NullPointerException class java.util.NoSuchElementException class java.io.IOException (voir transparent suivant !) Ici prochainement, vos exceptions !
La sous-hiérarchie « IOException » Pour gérer toute exception liée aux E/S il suffit de rattraper IOException On peut traiter de façon spécifique des cas particuliers (ex. les exceptions FileNotFoundException et EOFException).
Spécification des exceptions Une méthode doit déclarer explicitement les exceptions qu’elle est susceptible de lever : public void f(…) throws E1, E2 { … } le corps de f ne peut lever que les exceptions de classe E1 ou E2 f doit se préoccuper des exceptions levées par les méthodes qu’elle appelle soit en les rattrapant soit en les propageant (et en le déclarant dans son en-tête) L’appelant de f sait donc contre quelles exceptions se prémunir… Toute redéfinition de f dans les sous-classes devra respecter ce profil …mais rien n’empêche de lever une exception plus spécifique (sous-classe) Exemple: toute redéfinition de equals(Object O) ne peut lever que des instances (au sens large) de RuntimeException ou Error. La règle s’applique aussi à la méthode main !
Spécification des exceptions (exemple) import java.util.Calendar; public class RnDate { public RnDate(int year, int quantieme) throws ErreurDate { … refuse RnDate(2003, 366) ou RnDate(2003, -1) ! } // KO : l’exception n’est ni rattrapée, ni « déclarée » // Rejeté, même si en pratique l’exception ne peut pas se produire ! public static RnDate aujourdhui() { Calendar date = Calendar.getInstance(); return new RnDate(date.get(Calendar.YEAR), date.get(Calendar.DAY_OF_YEAR));
Spécification des exceptions (suite) A titre dérogatoire, il est facultatif de spécifier la levée des exceptions des hiérarchies Error et RunTimeException public void f(…) { … } ne peut donc lever/propager que des exceptions de classe Error ou RunTimeException, difficilement rattrapables public void f(…) throws Throwable { } peut lever des exceptions arbitraires mais laisse une (trop) lourde charge à ses appelants et est peu coopérative
Levée, récupération et propagation d’exceptions Une rupture du déroulement normal du programme… : arrêt du déroulement normal du programme pour rechercher dynamiquement un traite-exception adapté Ce traitement remplace le traitement normalement effectué, sans possibilité de retour… Levée implicite par la JVM ou une classe existante: String[] tab = new String[0]; Integer I; … ; System.out.println(Integer.intValue(I)); new Arraylist().iterator().next(); Levée explicite par le code du programmeur: throw new MonException();
Levée, récupération et propagation d’exceptions try { f1(); … fN(); } // bloc protégé catch (E1 v) { instructions } // des traites-exceptions (« handlers »)… … catch (Ep v) { instructions } // … en nombre quelconque Les catch sont examinés dans l’ordre jusqu’à trouver le premier qui correspond (modulo le sous-typage). Le compilateur vérifie que tout catch est accessible. Le traite-exception retenu remplace la fin du traitement du bloc try et continue à sa suite, avec les mêmes contraintes (type d’une valeur retournée, exceptions levées, etc.) A vous de conserver suffisamment d’information sur le contexte pour traiter l’exception (quel appel fi() a provoqué l’exception ?)
Récupération d’exceptions (suite) Un catch peut lui-même lever une exception la même : ou une autre : catch (Exception e) { catch (Exception e) { traitement local traitement local throw e; throw newMonException(); } } mais une exception levée dans un catch n’est jamais rattrapable dans le même bloc try/catch. Les try/catch peuvent englober d’autres blocs try/catch En l’absence de catch adapté, propagation automatique de l’exception au bloc englobant (où la même recherche de bloc catch aura lieu)
Récupération d’exceptions (fin) Attention à la portée des try/catch : f(Iterator it) { f(Iterator it) { try { while (true) while (true) try { it.next(); it.next(); } catch (…) { … } } catch (…) { … } } }
Levée, récupération et propagation d’exceptions Recherche, selon l’imbrication des try/catch, d’un traite-exception adapté Si on en trouve un : l’exécution continue à partir de ce point de la méthode Sinon, l’exécution de la méthode est interrompue et l’exception est signalée chez l’appelant, au point d’appel… On remonte donc dynamiquement la chaîne d’appels de méthodes jusqu’à éventuellement la méthode main initiale Si l’exception n’est jamais rattrapée, il y a interruption du programme… On ne rattrape pas si on ne sait pas quoi en faire… On ne rattrape pas si c’est juste pour propager !
Exceptions et constructeurs Attention aux exceptions dans les constructeurs : Dans quel état laisse-t-on l’instance concernée (initialisation partielle) ? Qu’est-ce que l’utilisateur pourra faire avec l’instance concernée ? Une exception levée dans un constructeur n’est jamais récupérable dans un autre constructeur public RnDate(int year, int quant) throws ErreurDate { … } public RnDate() { this(year, today); // Comment la rattraper ? Même problème pour la liaison constructeur sous-classe/super-classe ! Le constructeur de la sous-classe peut avoir des exceptions différentes…
La clause « finally » try { instructions } catch (Exception1 e1) { … } … … … // traites-exceptions optionnels } catch (ExceptionN eN) { } finally { instructions } // clause optionnelle Rôle : garantir qu’un fragment de code est exécuté dans tous les cas, normaux ou avec des exceptions, rattrapées ou non ! Précautions d’usage : attention aux doubles traitements attention à ne pas lever d’exceptions dans le bloc finally !
La clause « finally » : exemple class MonException extends Exception {} public class TestFinally { // adapté de B. Eckel: "Thinking in Java" public static void main(String[] args) { int count = 0; while (true) try { if (count == 0) { count++; throw new MonException(); } else if (count == 2) throw new NullPointerException(); else { count++; System.out.println("Cas normal"); } } catch (MonException e) { System.out.println("Dans le traite-exception"); } finally { System.out.println("Dans le bloc finally");} } Question : quels sont les affichages réalisés ?
La clause « finally » dans les boucles … class MonException extends Exception {} public class TestFinally2 { // adapte de B. Eckel: "Thinking in Java" public static void main(String[] args) { int count = 0; while (count < 10) try { count++; if (count % 2 == 0) continue; else if (count == 7) break; else System.out.println("Cas normal: " + count); } finally { // Exécuté dans les 3 cas ... System.out.println("Dans le bloc finally: " + count); }
Eléments méthodologiques On anticipe (i.e. on évite l’exception par un test explicite) ou on rattrape l’exception ? Privilégier la correction du code et sa lisibilité Une partie intégrante de la conception d’une classe Qui lève quoi ? Aussi important que les types des paramètres ! Quel niveau de détail : quelle interface (attributs, méthodes) ? Quelle hiérarchie dans les exceptions ?
Eléments méthodologiques : qui rattrape quoi ? Idéalement : on ne rattrape une exception que si on sait quoi en faire. Halte aux rattrapages abusifs et aux messages d’erreur intempestifs ! Exemple: une pile d’éléments de capacité bornée est pleine. L’utilisateur essaie d’empiler un nouvel élément. Vous faites quoi ? En phase de mise au point : il vaut mieux ne pas rattraper les exceptions qui ne devraient pas se produire (=> facilite la mise au point grâce à la trace de propagation) En production: on peut préférer un code plus « robuste » : on rattrape, à condition de savoir comment repartir du bon pied !
Eléments méthodologiques : qui rattrape quoi ? (suite) Il est courant de rattraper une exception pour en lever une autre à la place (éventuellement après un traitement local) public RendezVous(RnDate d, int q, String s, Person p) throws ErreurRendezVous { if (q < 1) throw new ErreurRendezVous ("erreurDurée"); try { this.deb = d; this.raison = s, this.qui = p; this.fin = new RnDate(d.annee(), d.quantieme() + q);// OK ? } catch (ErreurDate e) { p.prevenir("pas pendant le reveillon"); throw new ErreurRendezVous ("erreurAnnée"); } Il reste des éléments de choix : une ou plusieurs exception pour les rendez-vous ? Quel rapport hiérarchique entre les exceptions des rendez-vous, intervalles et dates ?
Eléments méthodologiques (fin) Qu’est-ce qu’un cas exceptionnel ? Exemple: la méthode add(Object O) de l’interface Collection : que se passe-t-il si on ajoute un élément ? Certaines collections admettent les doubles (ArrayList) D’autres collections n’ont jamais de doubles (TreeSet) Certaines collections ont des contraintes supplémentaires TreeSet : ne traite que les éléments munis d’une (même) relation d’ordre d’autres refusent d’ajouter null. D’autres enfin refusent d’implémenter add (ou remove) Toutes doivent être vues comme des sous-classes de Collection. Leurs versions de add doit se faire à profil d’exception constant. Quelles exceptions prévoyez-vous ? Pour quels cas de figure ?
Eléments méthodologiques : Exemple (fin) Un garde-fou à garantir : après C.add(e), si l’appel de méthode s’est bien passé, e doit être dans C ! Il n’est pas « exceptionnel » d’ajouter un élément à une collection qui le contient : add renvoie true si l’ajout est possible et C n’a pas été modifiée add renvoie false si l’ajout est possible et C n’a pas été modifiée add lève une exception si l’ajout est impossible : UnsupportedOperationException pour celles qui n’implémentent pas add ClassCastException pour celles qui ont des contraintes « de type » IllegalArgumentException pour d’autres contraintes sur les éléments Ces trois classes d’exception sont classées dans RuntimeException pour ne pas avoir à être spécifiées explicitement. On peut donc jouer sur la valeur de retour et/ou sur la levée d’exception !