IX. Gestion des erreurs avec les Exceptions▲
La philosophie de base de Java est « qu'un code mal formé ne sera pas exécuté ».
L'instant idéal pour détecter une erreur est à la compilation, bien avant que vous essayiez d'exécuter le programme. Malheureusement, toutes les erreurs ne peuvent être détectées à cette étape. Les problèmes restants doivent être gérés à l'exécution avec un certain formalisme qui permet à l'initiateur de l'erreur de passer une information suffisante à un récepteur qui va savoir comment traiter proprement cette anomalie.
En C et dans d'autres langages plus anciens, il pouvait y avoir plusieurs formalismes, et il était généralement établi comme des conventions et pas comme partie intégrante du langage de programmation. La gestion des erreurs se faisant typiquement en retournant par exemple une valeur spécifique ou en positionnant un drapeau, le récepteur avait la charge de tester ces données et de déterminer qu'il y avait eu une anomalie. Malgré cela avec le temps, on se rendit compte que les programmeurs qui utilisaient des librairies avaient tendance à se croire infaillibles « oui des erreurs peuvent se produire chez les autres, mais pas dans mon code ! ». Alors, de façon quasi naturelle, ils ne souhaitaient pas tester les conditions d'erreur (elles étaient parfois trop grossières pour être testées). (40) Si vous étiez assez méticuleux pour tester toutes les conditions d'erreur à chaque appel de fonction votre code pouvait devenir d'une illisibilité cauchemardesque. Parce que les programmeurs avaient toujours la possibilité de coupler un système avec ces langages, ils étaient réticents à admettre la vérité : cette approche de la gestion des erreurs était un obstacle à la création de grands programmes, robustes et faciles à maintenir.
La solution est d'extirper la nature occasionnelle de la gestion des erreurs et de renforcer le formalisme. Cette solution a une longue histoire, puisque l'implémentation de la gestion des erreurs remonte aux systèmes d'exploitation des années soixante et même au « on error goto » du BASIC. Mais la gestion des erreurs du C++ était basée sur ADA, et Java est basé sur le C++ (bien qu'il ressemble plus a du Pascal Objet).
Le « Mot » exception est pris dans le sens « je fais exception de ce cas ». À l'instant où le problème se produit, vous pouvez ne pas savoir quoi en faire, mais vous savez que vous ne pouvez pas continuer sans vous en soucier ; vous devez vous arrêter et quelqu'un quelque part doit savoir que faire. Donc vous transmettez le problème dans un contexte plus général ou quelqu'un est qualifié pour prendre la décision appropriée (comme dans une chaîne de commandement).
L'autre apport significatif de la gestion des exceptions est qu'elle simplifie le code de la gestion des erreurs. Au lieu de tester une condition d'erreur et la traiter à différents endroits de votre programme, vous n'avez plus besoin de placer le code de gestion des erreurs à l'appel de la méthode (puisque l'exception garantira que quelqu'un la lèvera) et vous avez juste besoin de traiter le problème à un seul endroit, appelé Gestionnaire d'Exception (Exception Handler). Cette gestion vous permet d'économiser du code, et de séparer le code que vous voulez exécuter du code qui doit être exécuté quand des erreurs se produisent. Généralement, la lecture l'écriture et le débogage du code est plus claire avec les exceptions qu'avec l'ancienne méthode.
La gestion des exceptions étant supportée par le Compilateur JAVA, il n'y a que quelques exemples qui peuvent être écrits dans ce livre sans apprendre le mécanisme de gestion des exceptions. Ce chapitre présente le code dont vous avez besoin pour gérer de façon adéquate les exceptions, et la façon de générer vos propres exceptions si une de vos méthodes venait à ne pas fonctionner.
IX-A. Les exceptions de base▲
Une condition exceptionnelle est un problème qui interdit de continuer la méthode ou la partie de code que vous êtes en train d'exécuter. Il est important de distinguer une condition exceptionnelle d'un problème normal, dans lequel vous avez assez d'informations dans le contexte courant pour faire face à la difficulté. Avec une condition exceptionnelle, vous ne pouvez pas continuer l'exécution, car vous n'avez pas suffisamment d'informations pour la traiter dans le contexte courant. Tout ce que vous pouvez faire est de sortir du contexte courant et de régler ce problème dans un contexte de plus haut niveau. C'est ce qui se passe quand vous générez une exception.
Un exemple simple est la division. Si vous vous apprêtez à diviser par zéro, cela vaut la peine de vérifier cette condition. Mais que cela signifie-t-il d'avoir le dénominateur à zéro ? Peut-être savez-vous, dans le contexte de cette méthode particulière, comment agir avec un dénominateur égal à zéro. Mais s'il s'agit d'une valeur inattendue, vous ne pouvez pas la traiter et vous devez donc générer une exception plutôt que de continuer l'exécution.
Quand vous générez une exception, plusieurs actions sont déclenchées. Premièrement, l'objet exception est créé de la même manière que tout objet Java : sur le tas ; avec new. Ensuite le chemin courant d'exécution (celui que vous ne pouvez pas continuer) est stoppé et la référence à l'objet exception est sortie du contexte courant. À cet instant le mécanisme de gestion des exceptions prend la main et commence à chercher un endroit approprié pour continuer à exécuter le programme. Cet endroit est le gestionnaire d'exception (exception handler), dont la tâche est de résoudre le problème de façon à ce que le programme puisse essayer ou de lancer une autre méthode pour traiter le problème ou juste de continuer.
Pour prendre un exemple simple, considérons une instance d'objet appelée t. Il est possible qu'il vous soit passé une référence qui n'ait pas été initialisée, donc vous pouvez vouloir vérifier son initialisation avant d'appeler une méthode sur cette instance. Vous pouvez envoyer l'information sur l'erreur dans un contexte plus général en créant un objet représentant votre information et en la « lançant » depuis votre contexte. Cette action s'appelle lever une exception. Cette action ressemble à ceci :
if
(
t ==
null
)
throw
new
NullPointerException
(
);
Ce code lève l'exception, qui vous permet, dans le contexte courant, de vous décharger de la responsabilité pour penser à la question plus loin. C'est juste par magie traité à un autre endroit. Nous montrerons précisément, où, par la suite.
IX-A-1. Les paramètres des Exceptions▲
Comme pour tout objet en Java, vous créez des exceptions dans la pile [Heap] en utilisant new, qui alloue de l'espace mémoire et appelle un constructeur. Il y a deux constructeurs dans toutes les exceptions standards : le premier est le constructeur par défaut et le second prend une chaîne de caractères comme argument ainsi vous pouvez placer des informations pertinentes dans l'exception :
throw
new
NullPointerException
(
"t = null"
);
Cette chaîne de caractères peut être extraite ultérieurement de différentes façons, comme nous allons le voir.
Le mot-clé throw déclenche une série d'événements relativement magiques. Typiquement, vous allez d'abord utiliser new pour instancier un objet qui représente la condition d'erreur. Vous transmettez la référence résultante au throw. L'objet est en effet retourné par la méthode, même si ce n'est pas ce type d'objet que la méthode doit renvoyer. Une vision simpliste de la gestion des exceptions est de la considérer comme un mécanisme de retour, mais vous allez au-devant de problèmes si vous poursuivez cette analogie trop loin. Vous pouvez aussi sortir de l'exécution normale en lançant une exception. Mais une valeur est retournée et la méthode ou l'environnement se termine.
Toute similitude avec un retour ordinaire d'une méthode s'arrête ici parce que l'endroit où vous arrivez est complètement différent de celui d'un retour normal d'une méthode. (Vous atterrissez chez le gestionnaire d'exception approprié qui peut être très éloigné, plusieurs niveaux plus bas sur la pile d'appels, de l'endroit où l'exception a été générée.)
De plus, vous pouvez lancer n'importe quel type d'objet Throwable (la classe exception principale). Ainsi vous générerez une classe d'exception pour chaque type d'erreur. L'information à propos de l'erreur est contenue à la fois dans l'objet exception et implicitement dans le nom de l'objet exception choisi, afin que quelqu'un dans le contexte supérieur sache quoi faire de votre exception. (Souvent, la seule information est le type de l'objet exception rien d'autre de significatif est stocké.)
IX-B. Attraper une exception▲
Si une méthode génère une exception, elle doit supposer qu'elle sera « interceptée » et levée. Un des avantages de la gestion des exceptions dans Java est qu'elle vous permet de vous concentrer sur le problème que vous essayez de résoudre à un endroit, et enfin de placer le code concernant les erreurs à un autre endroit.
Pour comprendre comment une exception est levée, vous devez comprendre le concept de région surveillée qui est une région de code qui peut générer des exceptions et qui est suivie par le code qui traite ces exceptions.
IX-B-1. Le bloc try▲
Si vous êtes à l'intérieur d'une méthode et que vous levez une exception (ou qu'une méthode appelée à l'intérieur de celle-ci lève une exception), cette méthode va sortir dans le processus de génération de l'exception. Si vous ne voulez pas qu'un throw provoque la sortie de la méthode vous pouvez spécifier un bloc spécial à l'intérieur de celle-ci qui va intercepter l'exception. C'est le bloc try appelé ainsi, car vous « essayez » (NDT : try en anglais) vos différents appels de méthode ici. Le bloc try est une section ordinaire précédée du mot clé try:
try
{
// Code pouvant générer des exceptions
}
Si vous vouliez tester les erreurs attentivement dans un langage de programmation qui ne supporte pas la gestion des exceptions, vous devriez entourer l'appel de chaque méthode avec un code de vérification de l'initialisation et de vérification des erreurs, même si vous appeliez la même méthode plusieurs fois. Avec la gestion des exceptions vous mettez tout dans le bloc try et capturez toutes les exceptions en un seul endroit. Cela signifie que votre code est beaucoup plus simple à lire et à écrire, car le bon code est séparé de celui de la gestion des erreurs.
IX-B-2. Les gestionnaires d'exception▲
Bien sûr, l'exception générée doit être traitée quelque part. Cet endroit est le gestionnaire d'exceptions, et il y en a un pour chaque type d'exception que vous voulez intercepter. Les gestionnaires d'exceptions sont placés juste derrière le bloc try et reconnaissables par le mot clé catch:
try
{
// Code pouvant générer des exceptions
}
catch
(
Type1 id1) {
// Traitement des exceptions de Type1
}
catch
(
Type2 id2) {
// Traitement des exceptions de Type2
}
catch
(
Type3 id3) {
// Traitement des exceptions de Type3
}
// etc...
Chaque clause du catch (gestionnaire d'exception) est comme une méthode qui ne prend qu'un argument d'un type particulier. Les identifiants (id1, id2, et ainsi de suite) peuvent être utilisés dans les gestionnaires d'exception, comme des paramètres de méthodes. Quelquefois vous n'utilisez pas l'identifiant, car le type de l'exception vous fournit assez d'informations pour la traiter, mais il doit toujours être présent.
Le gestionnaire doit être placé juste derrière le bloc try. Si une exception est générée, le mécanisme de gestion des exceptions va à la recherche du premier gestionnaire d'exception dont le type correspond à celui de l'exception. Cette recherche se termine quand une des clauses catch est exécutée. Seulement une clause catch sera exécutée ; ce n'est pas comme une instruction switch où vous devez positionner un break après chaque case pour empêcher les autres conditions de s'exécuter.
Remarquez qu'avec le bloc try, différentes méthodes peuvent générer la même exception, mais vous n'aurez besoin que d'un seul gestionnaire d'exception.
IX-B-2-a. Terminaison contre Restauration▲
Ce sont les deux modèles de base dans la théorie de la gestion des exceptions. Dans la terminaison (supportée par Java et C++), on suppose que l'erreur est si critique qu'il n'est pas possible de recommencer l'exécution à partir de l'endroit ou c'est produit l'exception. Celui qui génère l'exception décide qu'il n'y a pas de solution pour restaurer la situation, et ne veut pas revenir en arrière.
L'autre solution est appelée restauration. Cela signifie que le gestionnaire d'exception est censé agir pour corriger la situation, et ainsi la méthode incriminée est de nouveau exécutée avec un succès présumé la deuxième fois. Si vous voulez utiliser la restauration, cela signifie que vous voulez que l'exécution continue après le traitement de l'exception. Dans cette optique votre exception est résolue par un appel de méthode qui est la façon dont vous devriez résoudre vos problèmes en Java si vous voulez avoir un comportement de type restauration dans la gestion de vos exceptions. (C'est-à-dire, ne générez pas une exception ; appelez une méthode qui corrige le problème.) Sinon, placez votre bloc try dans un bloc while qui continuera à exécuter le bloc try tant que le résultat ne sera pas satisfaisant.
Historiquement, les programmeurs utilisant des systèmes d'exploitation qui supportaient la gestion des exceptions restauratrices finissaient par utiliser un code qui ressemblait à celui de la terminaison laissant de côté la restauration. Bien que la restauration ait l'air attractive au départ elle n'est pas aisée à mettre en œuvre. La raison dominante est le couplage que cela génère : votre gestionnaire d'exception doit être conscient de l'endroit où est générée l'exception et contenir du code générique indépendant de la localisation de sa génération. Cela rend le code difficile à écrire et à maintenir, surtout dans le cadre de grands systèmes où les exceptions peuvent surgir de nulle part.
IX-C. Créer vos propres exceptions▲
Vous n'êtes pas satisfait des exceptions existantes de Java. La hiérarchie d'exception du JDK ne peut pas prévoir toutes les erreurs que vous pouvez vouloir reporter, donc vous créez la vôtre afin d'encadrer un problème spécial qui peut survenir dans votre bibliothèque.
Pour créer votre propre classe d'exception, vous devez hériter d'une classe d'exception existante, préférablement une qui soit proche du contexte de votre nouvelle exception (même si ce n'est pas si souvent possible). La voie la plus triviale à emprunter pour créer un nouveau type d'exception est de laisser le compilateur créer le constructeur de base à votre place, ainsi cela ne requiert aucun code supplémentaire :
//: c09:SimpleExceptionDemo.java
// Héritage de vos propres exceptions.
import
com.bruceeckel.simpletest.*;
class
SimpleException extends
Exception {}
public
class
SimpleExceptionDemo {
private
static
Test monitor =
new
Test
(
);
public
void
f
(
) throws
SimpleException {
System.out.println
(
"Throw SimpleException from f()"
);
throw
new
SimpleException
(
);
}
public
static
void
main
(
String[] args) {
SimpleExceptionDemo sed =
new
SimpleExceptionDemo
(
);
try
{
sed.f
(
);
}
catch
(
SimpleException e) {
System.err.println
(
"Caught it!"
);
}
monitor.expect
(
new
String[] {
"Throw SimpleException from f()"
,
"Caught it!"
}
);
}
}
///:~
Le compilateur crée un constructeur par défaut, qui automatiquement (et de manière invisible) appelle le constructeur par défaut de la classe de base. Évidemment, dans ce cas vous n'obtiendrez pas un constructeur SimpleException(String), mais en pratique ce n'est pas très utilisé. Comme vous allez le voir, le plus important pour une exception est le nom de la classe, si important que le plus souvent une exception comme ci-dessous est satisfaisante.
Ici, le résultat est affiché sur le flux standard error de la console par System.err. C'est habituellement le meilleur moyen pour envoyer une information d'erreur plutôt que System.out, lequel peut être redirigé. Si vous envoyez la sortie sur System.err, elle ne sera pas redirigée quoiqu'il advienne de System.out. Ainsi, l'utilisateur sera plus susceptible de la remarquer.
Une classe d'exception peut aussi être créée avec un constructeur avec un argument String :
//: c09:FullConstructors.java
import
com.bruceeckel.simpletest.*;
class
MyException extends
Exception {
public
MyException
(
) {}
public
MyException
(
String msg) {
super
(
msg); }
}
public
class
FullConstructors {
private
static
Test monitor =
new
Test
(
);
public
static
void
f
(
) throws
MyException {
System.out.println
(
"Throwing MyException from f()"
);
throw
new
MyException
(
);
}
public
static
void
g
(
) throws
MyException {
System.out.println
(
"Throwing MyException from g()"
);
throw
new
MyException
(
"Originated in g()"
);
}
public
static
void
main
(
String[] args) {
try
{
f
(
);
}
catch
(
MyException e) {
e.printStackTrace
(
);
}
try
{
g
(
);
}
catch
(
MyException e) {
e.printStackTrace
(
);
}
monitor.expect
(
new
String[] {
"Throwing MyException from f()"
,
"MyException"
,
"%%
\t
at FullConstructors.f
\\
(.*
\\
)"
,
"%%
\t
at FullConstructors.main
\\
(.*
\\
)"
,
"Throwing MyException from g()"
,
"MyException: Originated in g()"
,
"%%
\t
at FullConstructors.g
\\
(.*
\\
)"
,
"%%
\t
at FullConstructors.main
\\
(.*
\\
)"
}
);
}
}
///:~
Le code ajouté est court : deux constructeurs qui définissent la façon dont MyException est instancié. Dans le second constructeur, le constructeur de la classe de base avec un String comme argument est invoqué explicitement par le mot clé super.
Dans les handlers, une des méthodes Throwable (depuis lesquelles Exception est héritée) est appelée : printStackTrace( ). Elle nous donne des informations à propos de la séquence des méthodes appelées pour arriver au point où l'exception est soulevée. Par défaut, ces informations sont dirigées vers le flux d'erreur standard, mais des versions surchargées vous permettent d'envoyer les résultats vers n'importe quel flux.
Le processus de création de vos propres exceptions peut aller plus loin. Vous pouvez ajouter d'autres constructeurs et membres :
//: c09:ExtraFeatures.java
// Pour plus d'embellissement des classes d'exception.
import
com.bruceeckel.simpletest.*;
class
MyException2 extends
Exception {
private
int
x;
public
MyException2
(
) {}
public
MyException2
(
String msg) {
super
(
msg); }
public
MyException2
(
String msg, int
x) {
super
(
msg);
this
.x =
x;
}
public
int
val
(
) {
return
x; }
public
String getMessage
(
) {
return
"Detail Message: "
+
x +
" "
+
super
.getMessage
(
);
}
}
public
class
ExtraFeatures {
private
static
Test monitor =
new
Test
(
);
public
static
void
f
(
) throws
MyException2 {
System.out.println
(
"Throwing MyException2 from f()"
);
throw
new
MyException2
(
);
}
public
static
void
g
(
) throws
MyException2 {
System.out.println
(
"Throwing MyException2 from g()"
);
throw
new
MyException2
(
"Originated in g()"
);
}
public
static
void
h
(
) throws
MyException2 {
System.out.println
(
"Throwing MyException2 from h()"
);
throw
new
MyException2
(
"Originated in h()"
, 47
);
}
public
static
void
main
(
String[] args) {
try
{
f
(
);
}
catch
(
MyException2 e) {
e.printStackTrace
(
);
}
try
{
g
(
);
}
catch
(
MyException2 e) {
e.printStackTrace
(
);
}
try
{
h
(
);
}
catch
(
MyException2 e) {
e.printStackTrace
(
);
System.err.println
(
"e.val() = "
+
e.val
(
));
}
monitor.expect
(
new
String[] {
"Throwing MyException2 from f()"
,
"MyException2: Detail Message: 0 null"
,
"%%
\t
at ExtraFeatures.f
\\
(.*
\\
)"
,
"%%
\t
at ExtraFeatures.main
\\
(.*
\\
)"
,
"Throwing MyException2 from g()"
,
"MyException2: Detail Message: 0 Originated in g()"
,
"%%
\t
at ExtraFeatures.g
\\
(.*
\\
)"
,
"%%
\t
at ExtraFeatures.main
\\
(.*
\\
)"
,
"Throwing MyException2 from h()"
,
"MyException2: Detail Message: 47 Originated in h()"
,
"%%
\t
at ExtraFeatures.h
\\
(.*
\\
)"
,
"%%
\t
at ExtraFeatures.main
\\
(.*
\\
)"
,
"e.val() = 47"
}
);
}
}
///:~
Un champ i a été ajouté, avec lequel une méthode qui lit cette valeur et un autre constructeur qui lui affecte une valeur. Encore, Throwable.getMessage( ) a été surchargé pour produire des messages plus détaillés. getMessage( ) joue le même rôle que toString( ) pour les classes d'exception.
Puisqu'une exception n'est qu'un autre type d'objet, vous pouvez continuer le processus d'amélioration des capacités de vos classes de gestion des exceptions. Gardez à l'esprit cependant, que tout cet habillage pourrait être perdu par les programmeurs clients utilisant vos paquetages, puisqu'ils pourraient simplement regarder l'exception être soulevée et rien de plus. (C'est ainsi que la plupart des exceptions de la bibliothèque JAVA sont utilisées.)
IX-D. La spécification des exceptions▲
En Java, vous êtes encouragé à fournir au programmeur qui appelle votre méthode la liste des exceptions pouvant être générées. C'est civilisé, puisque le programmeur client peut ainsi exactement savoir quel code écrire pour attraper toute exception potentielle. Bien sûr, si le code source est disponible, le programmeur client pourra y jeter un œil et rechercher l'instruction throw, mais souvent une bibliothèque est livrée sans les sources. Pour éviter que cela soit un problème, Java fournit une syntaxe (et vous oblige à l'utiliser) pour vous permettre d'informer formellement le programmeur client des exceptions qui sont générées par cette méthode, afin qu'il puisse les gérer. C'est la spécification des exceptions et cela fait partie intégrante de la déclaration de la méthode, elle apparaît après la liste des arguments.
La spécification des exceptions utilise un nouveau mot clé, throws, suivi par la liste des types d'exceptions possibles. Vos définitions de méthodes pourront donc ressembler à ceci :
void
f
(
) throws
TooBig, TooSmall, DivZero {
//...
Si vous écrivez
void
f
(
) {
// ...
cela signifie qu'aucune exception ne pourra être levée par la méthode (excepté les exceptions de type RuntimeException, qui peut être levée n'importe où sans spécification d'exception - nous verrons cela plus tard).
Vous ne pouvez pas mentir sur la spécification des exceptions. Si votre méthode génère des exceptions sans les gérer, le compilateur le détectera et vous dira que vous devez soit gérer ces exceptions soit indiquer par une spécification d'exception que cette exception peut être levée dans votre méthode. En forçant la spécification des exceptions de haut en bas, Java garantit qu'un certain niveau de justesse des exceptions peut être assuré au moment de la compilation.
Il y a un endroit où vous pouvez tricher : vous pouvez déclarer que vous levez une exception que vous ne levez pas réellement. Le compilateur vous prend au pied de la lettre, et force l'utilisateur de la méthode à agir comme si elle pouvait lever l'exception. Ceci a l'effet positif d'être un réceptacle pour cette exception, ainsi vous pourrez lever l'exception sans devoir modifier le code existant. C'est aussi important pour créer des classes abstraites et des interfaces dont les classes dérivées ou les implémentations peuvent avoir besoin de lever des exceptions.
Les exceptions qui sont vérifiées et appliquées lors de la compilation sont appelées exceptions contrôlées (NDT : en anglais checked exceptions).
IX-E. Attraper n'importe quelle exception▲
Il est possible de créer un gestionnaire qui intercepte n'importe quel type d'exception. Ceci est réalisé en interceptant la classe de base d'exception Exception. (Il existe d'autres types d'exception de base, mais Exception est pertinente pour tous les types d'activités de programmation):
catch
(
Exception e) {
System.err.println
(
"Caught an exception"
);
}
Ce code interceptera tout type d'exception, donc si vous l'utilisez, préférez le placer à la fin de votre liste de gestionnaires afin d'éviter la préemption à la place de certains de ces derniers. Sinon, le gestionnaire originellement chargé de préempter, cette exception sera sautée.
Puisque la classe Exception est la base de toutes les classes de gestion d'exception importantes pour le programmeur, vous n'obtenez pas beaucoup d'informations spécifiques à l'exception interceptée, mais vous pouvez appeler les méthodes qui viennent de son type de base Throwable :
String getMessage( )
String getLocalizedMessage( )
Fournit un message détaillé ou un message ajusté à la situation particulière.
String toString( )
Renvoie une courte description de l'objet Throwable, incluant un message détaillé s'il en existe un.
void printStackTrace( )
void printStackTrace(PrintStream)
void printStackTrace(java.io.PrintWriter)
Imprime l'objet Throwable ainsi que sa pile d'exécution. La pile d'exécution montre la séquence des appels aux méthodes vous ayant conduit à la levée de l'exception. La première version affiche ceci sur l'erreur standard, la seconde et la troisième affichent sur le flux au choix (dans le chapitre 12, vous comprendrez pourquoi plusieurs types de flux coexistent).
Throwable fillInStackTrace( )
Enregistre des informations issues de l'objet Throwable à propos de l'état courant des « stack frames ». C'est utile quand une application relance une erreur ou une exception (ceci sera approfondi plus loin).
De plus, vous sont mises à disposition quelques autres méthodes de la classe mère de Throwable n'étant rien d'autre que la classe Object (la classe mère de toutes les classes). Celle qui pourrait vous être le plus utile pour gérer des exceptions est getClass( ), qui renvoie un objet représentant la classe de cet objet. Vous pouvez interroger le nom de cet objet issu de Class avec getName( ). D'autres utilisations plus sophistiquées peuvent être accomplies avec les objets de Class qui ne sont pas nécessairement liées à la gestion des exceptions.
Une utilisation des méthodes de base de la classe Exception est montrée ci-dessous :
//: c09:ExceptionMethods.java
// Demonstrating the Exception Methods.
import
com.bruceeckel.simpletest.*;
public
class
ExceptionMethods {
private
static
Test monitor =
new
Test
(
);
public
static
void
main
(
String[] args) {
try
{
throw
new
Exception
(
"My Exception"
);
}
catch
(
Exception e) {
System.err.println
(
"Caught Exception"
);
System.err.println
(
"getMessage():"
+
e.getMessage
(
));
System.err.println
(
"getLocalizedMessage():"
+
e.getLocalizedMessage
(
));
System.err.println
(
"toString():"
+
e);
System.err.println
(
"printStackTrace():"
);
e.printStackTrace
(
);
}
monitor.expect
(
new
String[] {
"Caught Exception"
,
"getMessage():My Exception"
,
"getLocalizedMessage():My Exception"
,
"toString():java.lang.Exception: My Exception"
,
"printStackTrace():"
,
"java.lang.Exception: My Exception"
,
"%%
\t
at ExceptionMethods.main
\\
(.*
\\
)"
}
);
}
}
///:~
Voyez comment les méthodes fournissent successivement plus d'informations, chacune étant effectivement une évolution de la précédente.
IX-E-1. Relancer une exception▲
Il se peut que vous vouliez relancer une exception que vous venez juste d'intercepter, particulièrement quand vous utilisez Exception pour capter une exception génériquement. Puisque vous possédez déjà la référence de l'exception courante, vous pouvez simplement relancer la référence :
catch
(
Exception e) {
System.err.println
(
"Une exception a été levée"
);
throw
e;
}
Relancer une exception provoque le passage par le gestionnaire d'exception dans le contexte supérieur le plus proche. Toute autre clause catch pour le même bloc try sera ignorée. De plus, tout au sujet de l'objet exception est préservé, donc le gestionnaire au niveau supérieur qui intercepte le type spécifique d'exception peut extraire toutes les informations depuis cet objet.
Si vous relancez simplement l'exception courante, les informations que vous affichez à propos de cette exception dans printStackTrace( ) feront référence à l'exception d'origine, et non à l'endroit d'où vous la redirigez. Si vous voulez charger de nouvelles informations dans la pile des appels, fillInStackTrace( ) vous le permettra en renvoyant un objet Throwable qui est créé par ajout des informations actualisées dans la pile d'informations de l'ancien objet exception. Cela ressemble à ceci :
//: c09:Rethrowing.java
// Demonstrating fillInStackTrace()
import
com.bruceeckel.simpletest.*;
public
class
Rethrowing {
private
static
Test monitor =
new
Test
(
);
public
static
void
f
(
) throws
Exception {
System.out.println
(
"originating the exception in f()"
);
throw
new
Exception
(
"thrown from f()"
);
}
public
static
void
g
(
) throws
Throwable {
try
{
f
(
);
}
catch
(
Exception e) {
System.err.println
(
"Inside g(),e.printStackTrace()"
);
e.printStackTrace
(
);
throw
e; // 17
// throw e.fillInStackTrace(); // 18
}
}
public
static
void
main
(
String[] args) throws
Throwable {
try
{
g
(
);
}
catch
(
Exception e) {
System.err.println
(
"Caught in main, e.printStackTrace()"
);
e.printStackTrace
(
);
}
monitor.expect
(
new
String[] {
"originating the exception in f()"
,
"Inside g(),e.printStackTrace()"
,
"java.lang.Exception: thrown from f()"
,
"%%
\t
at Rethrowing.f(.*?)"
,
"%%
\t
at Rethrowing.g(.*?)"
,
"%%
\t
at Rethrowing.main(.*?)"
,
"Caught in main, e.printStackTrace()"
,
"java.lang.Exception: thrown from f()"
,
"%%
\t
at Rethrowing.f(.*?)"
,
"%%
\t
at Rethrowing.g(.*?)"
,
"%%
\t
at Rethrowing.main(.*?)"
}
);
}
}
///:~
Les lignes importantes sont commentées. Avec la ligne 17 décommentée (comme montrée ici), la sortie est ainsi affichée, donc la pile de traces d'exception se souvient toujours de son réel point d'origine, sans se soucier du nombre de fois auquel elle a été relancée.
Avec la ligne 17 commentée et la ligne 18 décommentée, fillInStackTrace( ) est utilisée en remplacement et le résultat est :
originating the exception in f()
Inside g(),e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:9)
at Rethrowing.g(Rethrowing.java:12)
at Rethrowing.main(Rethrowing.java:23)
Caught in main, e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.g(Rethrowing.java:18)
at Rethrowing.main(Rethrowing.java:23)
A cause de fillInStackTrace( ), la ligne 18 devient le nouveau point d'origine de l'exception. (À cela s'ajoutent des erreurs provenant de la méthode Test.expect()).
La classe Throwable doit apparaître dans la spécification de l'exception pour g( ) et main( ) du fait que fillInStackTrace( ) produit une référence à un objet Throwable. Puisque Throwable est une classe de base d'Exception, il est possible d'obtenir un objet qui soit Throwable, mais pas Exception, donc le gestionnaire pour Exception dans main( ) pourrait être manquant. Afin de s'assurer que tout est en ordre, le compilateur force une spécification d'exception pour Throwable. Par exemple, l'exception dans la suite du programme n'est pas interceptée par main( ):
//: c09:ThrowOut.java
// {ThrowsException}
public
class
ThrowOut {
public
static
void
main
(
String[] args) throws
Throwable {
try
{
throw
new
Throwable
(
);
}
catch
(
Exception e) {
System.err.println
(
"Caught in main()"
);
}
}
}
///:~
Il est aussi possible de relancer une exception différente depuis celle que vous avez interceptée. Si vous en décidez ainsi, vous obtenez un effet similaire à ce que vous auriez pu avoir en utilisant fillInStackTrace( ) - les informations à propos du point d'origine de l'exception sont alors perdues, et le contexte que vous quittez devient celui du nouveau lancement d'exception :
//: c09:RethrowNew.java
// Rethrow a different object from the one that was caught.
// {ThrowsException}
import
com.bruceeckel.simpletest.*;
class
OneException extends
Exception {
public
OneException
(
String s) {
super
(
s); }
}
class
TwoException extends
Exception {
public
TwoException
(
String s) {
super
(
s); }
}
public
class
RethrowNew {
private
static
Test monitor =
new
Test
(
);
public
static
void
f
(
) throws
OneException {
System.out.println
(
"originating the exception in f()"
);
throw
new
OneException
(
"thrown from f()"
);
}
public
static
void
main
(
String[] args) throws
TwoException {
try
{
f
(
);
}
catch
(
OneException e) {
System.err.println
(
"Caught in main, e.printStackTrace()"
);
e.printStackTrace
(
);
throw
new
TwoException
(
"from main()"
);
}
monitor.expect
(
new
String[] {
"originating the exception in f()"
,
"Caught in main, e.printStackTrace()"
,
"OneException: thrown from f()"
,
"
\t
at RethrowNew.f(RethrowNew.java:18)"
,
"
\t
at RethrowNew.main(RethrowNew.java:22)"
,
"Exception in thread
\"
main
\"
"
+
"TwoException: from main()"
,
"
\t
at RethrowNew.main(RethrowNew.java:28)"
}
);
}
}
///:~
L'exception finale sait seulement qu'elle provient de main( ) et non de f( ).
Vous n'avez jamais à vous soucier de la destruction des exceptions précédentes, ou quelque autre exception pour ce problème. Tous ces objets sont créés avec new, le ramasse-miettes les nettoie donc automatiquement.
IX-E-2. Enchaînement d'exceptions▲
Vous souhaitez souvent intercepter une exception et en lever une autre, mais toujours garder les informations de l'exception d'origine - ceci est appelé enchaînement d'exceptions. Avant le JDK 1.4, les programmeurs devaient écrire leur propre code pour préserver le contexte de l'exception d'origine, alors que maintenant toutes les sous-classes de Throwable peuvent prendre un objet cause dans leur constructeur. Cet objet cause est en fait l'exception d'origine, et par ce moyen, vous maintenez la pile de traces jusqu'au point d'origine, même si vous créez ou lancez une nouvelle exception de ce point.
Il est intéressant de noter que seules les sous-classes de Throwable qui fournissent l'argument cause dans leur constructeur sont les trois classes d'exception fondamentales Error (utilisées par la JVM pour reporter les erreurs système), Exception, et RuntimeException. Si vous voulez enchaîner d'autres types d'exception, préférez la méthode initCause( ) ) au constructeur.
Ci-dessous un exemple qui vous permet d'ajouter dynamiquement des champs à un objet DynamicFields au moment de l'exécution :
//: c09:DynamicFields.java
// A Class that dynamically adds fields to itself.
// Demonstrates exception chaining.
// {ThrowsException}
import
com.bruceeckel.simpletest.*;
class
DynamicFieldsException extends
Exception {}
public
class
DynamicFields {
private
static
Test monitor =
new
Test
(
);
private
Object[][] fields;
public
DynamicFields
(
int
initialSize) {
fields =
new
Object[initialSize][2
];
for
(
int
i =
0
; i <
initialSize; i++
)
fields[i] =
new
Object[] {
null
, null
}
;
}
public
String toString
(
) {
StringBuffer result =
new
StringBuffer
(
);
for
(
int
i =
0
; i <
fields.length; i++
) {
result.append
(
fields[i][0
]);
result.append
(
": "
);
result.append
(
fields[i][1
]);
result.append
(
"
\n
"
);
}
return
result.toString
(
);
}
private
int
hasField
(
String id) {
for
(
int
i =
0
; i <
fields.length; i++
)
if
(
id.equals
(
fields[i][0
]))
return
i;
return
-
1
;
}
private
int
getFieldNumber
(
String id) throws
NoSuchFieldException {
int
fieldNum =
hasField
(
id);
if
(
fieldNum ==
-
1
)
throw
new
NoSuchFieldException
(
);
return
fieldNum;
}
private
int
makeField
(
String id) {
for
(
int
i =
0
; i <
fields.length; i++
)
if
(
fields[i][0
] ==
null
) {
fields[i][0
] =
id;
return
i;
}
// No empty fields. Add one:
Object[][]tmp =
new
Object[fields.length +
1
][2
];
for
(
int
i =
0
; i <
fields.length; i++
)
tmp[i] =
fields[i];
for
(
int
i =
fields.length; i <
tmp.length; i++
)
tmp[i] =
new
Object[] {
null
, null
}
;
fields =
tmp;
// Reursive call with expanded fields:
return
makeField
(
id);
}
public
Object
getField
(
String id) throws
NoSuchFieldException {
return
fields[getFieldNumber
(
id)][1
];
}
public
Object setField
(
String id, Object value)
throws
DynamicFieldsException {
if
(
value ==
null
) {
// Most exceptions don't have a "cause" constructor.
// In these cases you must use initCause(),
// available in all Throwable subclasses.
DynamicFieldsException dfe =
new
DynamicFieldsException
(
);
dfe.initCause
(
new
NullPointerException
(
));
throw
dfe;
}
int
fieldNumber =
hasField
(
id);
if
(
fieldNumber ==
-
1
)
fieldNumber =
makeField
(
id);
Object result =
null
;
try
{
result =
getField
(
id); // Get old value
}
catch
(
NoSuchFieldException e) {
// Use constructor that takes "cause":
throw
new
RuntimeException
(
e);
}
fields[fieldNumber][1
] =
value;
return
result;
}
public
static
void
main
(
String[] args) {
DynamicFields df =
new
DynamicFields
(
3
);
System.out.println
(
df);
try
{
df.setField
(
"d"
, "A value for d"
);
df.setField
(
"number"
, new
Integer
(
47
));
df.setField
(
"number2"
, new
Integer
(
48
));
System.out.println
(
df);
df.setField
(
"d"
, "A new value for d"
);
df.setField
(
"number3"
, new
Integer
(
11
));
System.out.println
(
df);
System.out.println
(
df.getField
(
"d"
));
Object field =
df.getField
(
"a3"
); // Exception
}
catch
(
NoSuchFieldException e) {
throw
new
RuntimeException
(
e);
}
catch
(
DynamicFieldsException e) {
throw
new
RuntimeException
(
e);
}
monitor.expect
(
new
String[] {
"null: null"
,
"null: null"
,
"null: null"
,
""
,
"d: A value for d"
,
"number: 47"
,
"number2: 48"
,
""
,
"d: A new value for d"
,
"number: 47"
,
"number2: 48"
,
"number3: 11"
,
""
,
"A value for d"
,
"Exception in thread
\"
main
\"
"
+
"java.lang.RuntimeException: "
+
"java.lang.NoSuchFieldException"
,
"
\t
at DynamicFields.main(DynamicFields.java:98)"
,
"Caused by: java.lang.NoSuchFieldException"
,
"
\t
at DynamicFields.getFieldNumber("
+
"DynamicFields.java:37)"
,
"
\t
at DynamicFields.getField(DynamicFields.java:58)"
,
"
\t
at DynamicFields.main(DynamicFields.java:96)"
}
);
}
}
///:~
Chaque objet DynamicFields contient un tableau de pairs Object-Object. Le premier objet est un champ identifieur (un String), et le second est une valeur champ qui peut être de n'importe quel type à l'exception des types primitifs. Lorsque vous créez un objet, vous estimez le nombre de champs dont vous avez besoin. Quand vous appelez la méthode setField( ), soit elle trouve le champ existant par son nom ou en crée un et lui affecte votre valeur. Si cela s'exécute hors espace mémoire, cette méthode réserve un nouvel espace en créant un tableau de longueur une fois plus importante et copiant les anciens éléments ici. Si vous tentez de mettre à l'intérieur de ce tableau une valeur null, alors la méthode soulève une exception DynamicFieldsException en créant et utilisant initCause( ) pour insérer une cause NullPointerException.
Comme valeur de retour, la méthode setField( ) renvoie aussi l'ancienne valeur de cette position dans le champ en utilisant getField( ), qui pourrait lever une NoSuchFieldException. Si les programmeurs client appellent getField( ), alors ils sont responsables de la manipulation de NoSuchFieldException, mais si cette exception est lancée depuis setField( ), c'est une erreur de programmation, donc l'exception NoSuchFieldException est convertie en une RuntimeException via le constructeur prenant comme argument une cause.
IX-F. Exceptions Java standards▲
La classe Java Throwable décrit tout ce qui peut être lancé en tant qu'exception. Deux types d'objets Throwable généraux existent (« types d'objets » = « hérités de »). Error représente les erreurs de compilation et système dont que vous ne devriez pas vous souciez d'attraper (excepté dans des cas bien précis). Exception est le type de base qui peut être lancé depuis n'importe quelle méthode des classes de la bibliothèque standard de Java ou même depuis vos propres méthodes ainsi que suite à des accidents survenus pendant l'exécution. Donc le type de base intéressant pour le programmeur Java est habituellement Exception.
La meilleure façon de voir l'étendue des exceptions est de naviguer dans la documentation HTML de Java, que vous pouvez télécharger depuis java.sun.com. Il est conseillé de faire ceci au moins une fois pour se rendre compte de la variété des exceptions, mais vous verrez bientôt qu'il n'y a rien de spécial entre une exception et la suivante mis à part le nom. Encore, le nombre d'exceptions dans Java continue de grandir ; de toute façon, imprimer la liste des exceptions dans un livre serait sans intérêt. N'importe quelle bibliothèque que vous recevez d'un vendeur tiers vous fournira encore de nouvelles exceptions. La chose importante à comprendre est le concept ainsi que ce que vous devriez faire avec les exceptions.
L'idée basique est que le nom de l'exception représente le problème qui est survenu, et le nom de l'exception est censé être relativement explicite de lui-même. Les exceptions ne sont pas toutes définies dans java.lang ; d'autres sont créées pour supporter d'autres bibliothèques telles que util, net, and io, que vous pouvez voir depuis leur nom de classe complet ou en regardant depuis n'importe quelle classe dont elles héritent. Par exemple, toutes les exceptions d'entrée/sortie héritent de java.io.IOException.
IX-F-1. Le cas particulier RuntimeException▲
Le premier exemple de ce chapitre était
if
(
t ==
null
)
throw
new
NullPointerException
(
);
Cela peut être un peu horrifiant de penser que vous devez vérifier la valeur null sur chaque référence qui est passée à une méthode (puisque vous ne pouvez savoir si l'appelant vous a passé une référence valide). Heureusement, vous ne le faites pas. C'est un élément de la vérification standard de l'exécution que Java fait pour vous, et si un quelconque appel est fait à une référence null, Java lancera automatiquement une NullPointerException. Donc, l'extrait de code ci-dessus est toujours superflu.
Tout un groupe de types d'exception se place dans cette catégorie. Elles sont toujours lancées automatiquement par Java et vous n'avez pas besoin de les inclure dans vos spécifications d'exception. De façon assez pratique, elles sont toutes groupées sous une seule class nommée RuntimeException, ce qui est un exemple parfait d'héritage ; cela établit une famille de types qui ont des caractéristiques et comportements en commun. De plus, vous n'avez jamais besoin d'écrire une spécification d'exception disant qu'une méthode peut lancer une RuntimeException (ou tout autre type hérité de RuntimeException), car ce sont des exceptions non vérifiées. Parce qu'elles indiquent des bugs, vous n'interceptez généralement pas une RuntimeException - ceci est géré automatiquement. Si vous étiez forcé de vérifier des RuntimeException, votre code pourrait devenir trop lourd. Même si typiquement vous n'interceptez pas RuntimeException dans vos propres paquetages, vous pourriez choisir de lancer vous-même une RuntimeException.
Que se passe-t-il quand vous n'interceptez pas ce genre d'exceptions ? Puisque le compilateur n'impose pas de spécification d'exception pour elles, il est assez plausible qu'une RuntimeException pourrait remonter jusqu'à votre méthode main( ) sans être interceptée. Pour voir ce qui arrive dans un tel cas, essayez l'exemple suivant :
//: c09:NeverCaught.java
// Ignorer les RuntimeExceptions.
// {ThrowsException}
import
com.bruceeckel.simpletest.*;
public
class
NeverCaught {
private
static
Test monitor =
new
Test
(
);
static
void
f
(
) {
throw
new
RuntimeException
(
"From f()"
);
}
static
void
g
(
) {
f
(
);
}
public
static
void
main
(
String[] args) {
g
(
);
monitor.expect
(
new
String[] {
"Exception in thread
\"
main
\"
"
+
"java.lang.RuntimeException: From f()"
,
" at NeverCaught.f(NeverCaught.java:7)"
,
" at NeverCaught.g(NeverCaught.java:10)"
,
" at NeverCaught.main(NeverCaught.java:13)"
}
);
}
}
///:~
Vous voyez d'ores et déjà qu'une RuntimeException (ou n'importe quelle exception héritant d'elle) est un cas particulier, puisque le compilateur ne requiert nullement une spécification d'exception pour ces types.
La réponse est donc : si une RuntimeException remonte jusqu'au main( ) sans être interceptée, printStackTrace( ) est appelée pour cette exception alors que le programme prend fin.
Gardez à l'esprit que vous pouvez ignorer uniquement les exceptions de type RuntimeException (ainsi que ses sous-classes) dans votre code, puisque toute autre gestion est soigneusement garantie par le compilateur. La raison est qu'une RuntimeException représente une erreur de programmation :
- Une erreur que vous ne pouvez pas anticiper. Par exemple, un référence null hors de votre contrôle.
- Une erreur que vous, en tant que programmeur, devriez avoir vérifiée dans votre code (comme une ArrayIndexOutOfBoundsException pour laquelle vous auriez du faire attention à la taille du tableau). Une exception qui survient du point 1 devient souvent une question pour le point 2.
Vous pouvez voir l'énorme bénéfice d'avoir des exceptions dans ce cas, puisqu’elles vous aident lors du débogage.
Il est intéressant de remarquer que vous ne pouvez classer la manipulation des exceptions Java comme un outil ciblé sur un seul sujet. Oui, c'est destiné à manipuler ces agaçantes erreurs d'exécution qui surviendront du fait des forces hors du contrôle de votre code, mais c'est aussi essentiel pour certains types de bugs de programmation que le compilateur ne peut détecter.
IX-G. Effectuer le ménage avec finally▲
Il y a souvent une partie du code que vous voulez exécuter, qu'une exception soit générée ou pas au cœur d'un bloc try. Cela est généralement destiné à d'autres opérations que la récupération de la mémoire (puisque le ramasse-miettes s'en occupe). Pour obtenir cet effet, vous utilisez la clause finally(41) à la fin de tous les gestionnaires d'exception. L'image complète d'une section de gestion d'exception est :
try
{
// La région gardée : activités dangereuses
// qui peuvent lancer A, B ou C
}
catch
(
A a1) {
// Gestion pour la situation A
}
catch
(
B b1) {
// Gestion pour la situation B
}
catch
(
C c1) {
// Gestion pour la situation C
}
finally
{
// Activités qui arrivent tout le temps
}
Pour démontrer que la clause finally est toujours exécutée, essayer ce programme :
//: c09:FinallyWorks.java
// La clause finally est toujours exécutée.
import
com.bruceeckel.simpletest.*;
class
ThreeException extends
Exception {}
public
class
FinallyWorks {
private
static
Test monitor =
new
Test
(
);
static
int
count =
0
;
public
static
void
main
(
String[] args) {
while
(
true
) {
try
{
// Post incrément, zéro la première fois :
if
(
count++
==
0
)
throw
new
ThreeException
(
);
System.out.println
(
"No exception"
);
}
catch
(
ThreeException e) {
System.err.println
(
"ThreeException"
);
}
finally
{
System.err.println
(
"In finally clause"
);
if
(
count ==
2
) break
; // sortie du "while"
}
}
monitor.expect
(
new
String[] {
"ThreeException"
,
"In finally clause"
,
"No exception"
,
"In finally clause"
}
);
}
}
///:~
À partir de la sortie, vous pouvez voir que l'exception soit ou non levée, la clause finally est toujours exécutée.
Ce programme vous donne aussi une aide pour comprendre comment gérer le fait que Java (comme les exceptions en C++) ne vous permet pas de reprendre l'exécution de l'endroit où l'exception a été générée, comme indiqué plus tôt. Si vous placez votre bloc try dans une boucle, vous pouvez établir une condition qui doit être validée avant de continuer le programme. Vous pouvez aussi ajouter un compteur de type static ou d'autres moyens afin de permettre à la boucle d'essayer différentes approches avant l'abandon. De cette façon vous pouvez obtenir un plus grand niveau de robustesse de vos programmes.
IX-G-1. À quoi sert finally ?▲
Dans un langage sans ramasse-miettes et sans appel automatique des destructeurs (42) , finally est important parce qu'il permet au programmeur de garantir la libération de la mémoire indépendamment de ce qui se passe dans le bloc try. Cependant Java a un ramasse-miettes, ainsi libérer la mémoire n'est quasiment jamais un problème. De plus, il n'a pas de destructeurs à appeler. Quand donc avez-vous besoin d'utiliser finally en Java ?
La clause finally est nécessaire quand vous avez besoin de remettre à l'état original autre chose que la mémoire. C'est une sorte de nettoyage comme un fichier ouvert, une connexion réseau, quelque chose que vous avez dessiné à l'écran, ou un échange avec le monde extérieur comme modélisé dans l'exemple suivant :
//: c09:Switch.java
public
class
Switch {
private
boolean
state =
false
;
public
boolean
read
(
) {
return
state; }
public
void
on
(
) {
state =
true
; }
public
void
off
(
) {
state =
false
; }
}
///:~
//: c09:OnOffException1.java
public
class
OnOffException1 extends
Exception {}
///:~
//: c09:OnOffException2.java
public
class
OnOffException2 extends
Exception {}
///:~
//: c09:OnOffSwitch.java
// Pourquoi utilser finally?
public
class
OnOffSwitch {
private
static
Switch sw =
new
Switch
(
);
public
static
void
f
(
)
throws
OnOffException1,OnOffException2 {}
public
static
void
main
(
String[] args) {
try
{
sw.on
(
);
// Code qui peut lancer des exceptions
f
(
);
sw.off
(
);
}
catch
(
OnOffException1 e) {
System.err.println
(
"OnOffException1"
);
sw.off
(
);
}
catch
(
OnOffException2 e) {
System.err.println
(
"OnOffException2"
);
sw.off
(
);
}
}
}
///:~
L'objectif ici est d'être sûr que le Switch est « off » quand la méthode main( ) est terminée, ainsi sw.off( ) est placée à la fin du bloc try et à la fin de chaque gestionnaire d'exception. Cependant il est possible qu'une exception soit générée et qu'elle ne soit pas interceptée ici, ainsi l'appel à sw.off( ) serait manqué. Avec finally, vous pouvez placer le code de nettoyage du bloc try à un seul endroit :
//: c09:WithFinally.java
// Finally garanti le nettoyage.
public
class
WithFinally {
static
Switch sw =
new
Switch
(
);
public
static
void
main
(
String[] args) {
try
{
sw.on
(
);
// Code qui peut lancer des exceptions...
OnOffSwitch.f
(
);
}
catch
(
OnOffException1 e) {
System.err.println
(
"OnOffException1"
);
}
catch
(
OnOffException2 e) {
System.err.println
(
"OnOffException2"
);
}
finally
{
sw.off
(
);
}
}
}
///:~
Ici le sw.off( ) a été placé à un endroit unique où on est sûr qu'il sera exécuté quoi qu'il arrive.
Même dans le cas où l'exception n'est pas interceptée dans l'ensemble des clauses catch, finally sera exécuté avant que le mécanisme de gestion d'exception recherche un gestionnaire de plus haut niveau :
//: c09:AlwaysFinally.java
// Finally est toujours executé.
import
com.bruceeckel.simpletest.*;
class
FourException extends
Exception {}
public
class
AlwaysFinally {
private
static
Test monitor =
new
Test
(
);
public
static
void
main
(
String[] args) {
System.out.println
(
"Entering first try block"
);
try
{
System.out.println
(
"Entering second try block"
);
try
{
throw
new
FourException
(
);
}
finally
{
System.out.println
(
"finally in 2nd try block"
);
}
}
catch
(
FourException e) {
System.err.println
(
"Caught FourException in 1st try block"
);
}
finally
{
System.err.println
(
"finally in 1st try block"
);
}
monitor.expect
(
new
String[] {
"Entering first try block"
,
"Entering second try block"
,
"finally in 2nd try block"
,
"Caught FourException in 1st try block"
,
"finally in 1st try block"
}
);
}
}
///:~
L'étape finally sera aussi exécutée dans des situations avec les déclarations break et continue. Notez qu'avec le break étiqueté et le continue étiqueté, finally élimine le besoin de déclarer un goto en Java.
IX-G-2. Piège: l'exception perdue▲
Hélas, il existe une faille dans l'implémentation des exceptions en Java. Bien que les exceptions soient un indicateur d'évènement grave dans votre programme et ne devraient jamais être ignorées il est possible qu'une exception soit tout simplement perdue. Cela se produit dans une configuration spécifique utilisant une clause finally:
//: c09:LostMessage.java
// Comment une exception peut être perdue
// {ThrowsException}
import
com.bruceeckel.simpletest.*;
class
VeryImportantException extends
Exception {
public
String toString
(
) {
return
"Une exception très sérieuse"
;
}
}
class
HoHumException extends
Exception {
public
String toString
(
) {
return
"une exception sans gravité"
;
}
}
public
class
LostMessage {
private
static
Test monitor =
new
Test
(
);
void
f
(
) throws
VeryImportantException {
throw
new
VeryImportantException
(
);
}
void
dispose
(
) throws
HoHumException {
throw
new
HoHumException
(
);
}
public
static
void
main
(
String[] args) throws
Exception {
LostMessage lm =
new
LostMessage
(
);
try
{
lm.f
(
);
}
finally
{
lm.dispose
(
);
}
monitor.expect
(
new
String[] {
"Exception in thread
\"
main
\"
une exception sans gravité"
,
"
\t
at LostMessage.dispose(LostMessage.java:24)"
,
"
\t
at LostMessage.main(LostMessage.java:31)"
}
);
}
}
///:~
Vous pouvez constater qu'il n'existe aucune trace de la VeryImportantException, qui est tout simplement remplacée par l'exception HoHumException dans la clause finally. C'est un piège plutôt dangereux, étant donné que cela signifie qu'une erreur peut être complètement perdue, et d'une façon bien plus subtile et difficile à détecter que dans l'exemple précédent. Par opposition, C++ considère le cas dans lequel une seconde exception est renvoyée avant que la première ne soit traitée comme une grave erreur de développement. Peut-être qu'une future version de Java corrigera le problème (d'un autre côté, vous allez typiquement encapsuler toute méthode qui renvoie une exception telle que dispose( ) dans une clause try-catch).
IX-H. Restrictions d'Exceptions▲
Quand vous surchargez une méthode, vous ne pouvez générer que les exceptions qui ont été spécifiées dans la classe de base de la version de la méthode. C'est une restriction très utile, puisqu'elle garantit que le code qui fonctionne avec la version de la méthode de la classe de base fonctionnera automatiquement avec tous les objets dérivant de cette classe (un concept fondamental de la POO, bien sûr), incluant les exceptions.
Cet exemple démontre les types de restrictions imposées (à la compilation) pour les exceptions :
//: c09:StormyInning.java
// Surcharger des méthodes peut lever seulement les exceptions
// spécifiées dans les versions de leur classe de base, ou les exceptions
// dérivées depuis les exceptions de la classe de base.
class
BaseballException extends
Exception {}
class
Foul extends
BaseballException {}
class
Strike extends
BaseballException {}
abstract
class
Inning {
public
Inning
(
) throws
BaseballException {}
public
void
event
(
) throws
BaseballException {
// N'a en réalité rien à lever
}
public
abstract
void
atBat
(
) throws
Strike, Foul;
public
void
walk
(
) {}
// Lève les exceptions non vérifiées
}
class
StormException extends
Exception {}
class
RainedOut extends
StormException {}
class
PopFoul extends
Foul {}
interface
Storm {
public
void
event
(
) throws
RainedOut;
public
void
rainHard
(
) throws
RainedOut;
}
public
class
StormyInning extends
Inning implements
Storm {
// OK pour ajouter de nouvelles exceptions pour les constructeurs, mais vous
// devez le faire avec le constructeur de base des exceptions :
public
StormyInning
(
)
throws
RainedOut, BaseballException {}
public
StormyInning
(
String s)
throws
Foul, BaseballException {}
// Les méthodes normales doivent se conformer à la classe de base :
//! void walk() throws PopFoul {} //Erreur de compilation
// L'Interface NE PEUT PAS ajouter des exceptions pour
// les méthodes existantes de la classe de base :
//! public void event() throws RainedOut {}
// Si la méthode n'existe pas déjà dans la
// classe de base, l'exception est OK :
public
void
rainHard
(
) throws
RainedOut {}
// Vous pouvez choisir de ne pas lever des exceptions,
// même si la version de base le fait :
public
void
event
(
) {}
// Surcharger des méthodes peut lever des exceptions héritées :
public
void
atBat
(
) throws
PopFoul {}
public
static
void
main
(
String[] args) {
try
{
StormyInning si =
new
StormyInning
(
);
si.atBat
(
);
}
catch
(
PopFoul e) {
System.err.println
(
"Pop foul"
);
}
catch
(
RainedOut e) {
System.err.println
(
"Rained out"
);
}
catch
(
BaseballException e) {
System.err.println
(
"Generic baseball exception"
);
}
// Strike n'est pas levé dans la version dérivée.
try
{
// Que se passe-t-il si on fait un transtypage ascendant ?
Inning i =
new
StormyInning
(
);
i.atBat
(
);
// Vous devez intercepter les exceptions depuis la
// version de la classe de base de la méthode :
}
catch
(
Strike e) {
System.err.println
(
"Strike"
);
}
catch
(
Foul e) {
System.err.println
(
"Foul"
);
}
catch
(
RainedOut e) {
System.err.println
(
"Rained out"
);
}
catch
(
BaseballException e) {
System.err.println
(
"Generic baseball exception"
);
}
}
}
///:~
Dans Inning, vous pouvez voir qu'à la fois le constructeur et la méthode event( ) déclarent qu'elles peuvent lever une exception, mais elles ne le feront jamais. C'est tout à fait légitime puisqu'elles vous laissent la possibilité de forcer l'utilisateur à intercepter toutes les exceptions qui peuvent être ajoutées dans les versions surchargées de event( ). Le même principe s'applique pour les méthodes abstract, comme nous l'avons vu dans atBat( ).
L'interface Storm est intéressante parce qu'elle contient une méthode (event( )) qui est définie dans Inning, et une autre qui ne l'est pas. Toutes génèrent un nouveau type d'exception, RainedOut. Quand StormyInning extends Inning et implements Storm, vous verrez que la méthode event( ) de Storm ne peut pas changer l'interface de l'exception de event( ) dans Inning. Une fois encore, cela est sensé, car sinon vous ne savez jamais si vous interceptez la bonne chose lorsque vous travaillez avec la classe de base. Bien sûr, si une méthode décrite dans une interface n'est pas dans la classe de base, telle que rainHard( ), alors il n'y a aucun problème lorsqu'elle génère des exceptions.
La restriction sur les exceptions ne s'applique pas aux constructeurs. Vous pouvez voir dans StormyInning un constructeur qui peut générer toutes les exceptions qu'il veut sans être contraint par ce que génère le constructeur de la classe de base. Cependant, étant donné qu'un constructeur de la classe de base doit toujours être appelé d'une manière ou d'une autre (ici, le constructeur par défaut est appelé automatiquement), le constructeur de la classe dérivée doit déclarer toutes exceptions du constructeur de la classe de base dans ses spécifications d'exception. Notez qu'un constructeur d'une classe dérivée ne peut pas intercepter les exceptions levées par le constructeur de sa classe de base.
La raison pour laquelle StormyInning.walk( ) ne compilera pas est qu'elle génère une exception, alors que Inning.walk( ) n'en génère pas. Si cela était permis, alors vous pourriez écrire le code qui appelle Inning.walk( ) et qui n'aurait pas à gérer une seule exception, mais lorsque vous substitueriez à Inning un objet d'une de ses classes dérivées, des exceptions seraient levées et votre code serait interrompu. En forçant les méthodes des classes dérivées à se conformer aux spécifications d'exceptions des classes de base, la substituabilité des objets est maintenue.
La méthode surchargée event( ) montre qu'une méthode héritée d'une classe peut choisir de ne pas générer d'exceptions, même si la méthode de la classe de base le fait. Ceci est de nouveau bon puisque cela ne rend caduc aucun code qui a été écrit présumant que la classe de base génère des exceptions. La même logique s'applique à atBat( ), qui génère une exception PopFoul, une exception qui est dérivée de l'exception Foul générée par la version de la classe de base de atBat( ). De cette façon, si quelqu'un écrit du code qui fonctionne avec Inning et appelle atBat( ), il doit intercepter l'exception Foul. Puisque PopFoul hérite de Foul, le gestionnaire d'exception interceptera PopFoul.
Le dernier point intéressant est dans le main( ). Ici, vous pouvez voir que si vous avez exactement à faire à un objet StormyInning, le compilateur vous oblige à intercepter seulement les exceptions qui sont spécifiques à cette classe, mais si vous faites un transtypage ascendant (upcast) vers le type de base, le compilateur vous oblige (avec raison) à gérer toutes les exceptions du type de base. Toutes ces contraintes produisent du code de gestion des exceptions beaucoup plus robuste. (43)
Il est utile de se rendre compte que bien que les spécifications d'exceptions sont renforcées au moment de l'héritage par le compilateur, les spécifications d'exceptions ne sont pas une partie du type d'une méthode, qui comprend seulement le nom de la méthode et les types d'arguments. Par conséquent, vous ne pouvez pas surcharger une méthode basée sur des spécifications d'exceptions. De plus, il n'est pas obligatoire qu'une exception spécifiée dans méthode d'une classe de base soit présente dans la méthode d'une classe en héritant. C'est assez différent des règles de l'héritage, où une méthode qui existe dans une classe de base doit aussi exister dans sa classe dérivée. Autrement dit, l' « interface de spécification des exceptions » pour une méthode particulière peut se restreindre lors de l'héritage et la redéfinition, mais elle ne peut pas s'élargir - c'est précisément le contraire de la règle pour l'interface de classe au cours de l'héritage.
IX-I. Les Constructeurs▲
Lorsque vous écrivez un code contenant des exceptions, il est particulièrement important de toujours vous demander « Si une exception est levée, sera-t-elle traitée comme il se doit ? » La plupart du temps c'est relativement sûr, mais dans les constructeurs il y a un problème. Le constructeur crée l'objet dans un état de départ « sain » cependant il peut procéder à des opérations - comme l'ouverture d'un fichier - qui ne seront pas terminées tant que l'utilisateur n'aura pas fini de travailler avec l'objet et appelé une méthode spécifique de libération. Si vous levez une exception dans un constructeur, ces mécanismes de libération peuvent ne pas être exécutés correctement. Ce qui signifie que vous devez être extrêmement prudent lorsque vous écrivez votre constructeur.
Comme vous venez de découvrir la clause finally, vous pourriez penser que c'est la solution appropriée. Mais ce n'est pas aussi simple que cela, car finally est toujours exécutée, même dans des situations où vous ne voulez pas que la méthode de libération soit appelée. Ainsi, si vous exécutez le code de libération dans le finally, vous devrez placer une sorte d'indicateur (flag) lorsque l'exécution du constructeur se termine correctement, ce qui vous permet de ne rien faire dans le finally si le flag est mis. Étant donné que cette solution manque d'élégance (deux bouts de code liés se trouvent à des endroits différents), il est préférable d'éviter de faire ce genre de traitement dans le finally à moins d'y être forcé.
Dans l'exemple qui suit, une classe nommée InputFile est créée. Elle permet d'ouvrir un fichier et de le lire ligne par ligne (en les convertissant en String). Elle utilise les classes FileReader et BufferedReader de la librairie I/O standard de Java dont nous reparlerons dans le chapitre 12, mais qui sont suffisamment simples à appréhender pour que vous n'ayez pas de problème à comprendre leur fonctionnement de base :
//: c09:Cleanup.java
// Comment utiliser les exceptions dans les constructeurs
import
com.bruceeckel.simpletest.*;
import
java.io.*;
class
InputFile {
private
BufferedReader in;
public
InputFile
(
String fname) throws
Exception {
try
{
in =
new
BufferedReader
(
new
FileReader
(
fname));
// Code pouvant lever des exceptions
}
catch
(
FileNotFoundException e) {
System.err.println
(
"Impossible d'ouvrir le fichier "
+
fname);
// Fichier non ouvert, inutile de le refermer
throw
e;
}
catch
(
Exception e) {
// Toutes les autres exceptions doivent le refermer
try
{
in.close
(
);
}
catch
(
IOException e2) {
System.err.println
(
"in.close() a échoué"
);
}
throw
e; // relance l'exception
}
finally
{
// Ne pas le fermer ici !!!
}
}
public
String getLine
(
) {
String s;
try
{
s =
in.readLine
(
);
}
catch
(
IOException e) {
throw
new
RuntimeException
(
"readLine() a échoué"
);
}
return
s;
}
public
void
dispose
(
) {
try
{
in.close
(
);
System.out.println
(
"dispose() a réussi"
);
}
catch
(
IOException e2) {
throw
new
RuntimeException
(
"in.close() a échoué"
);
}
}
}
public
class
Cleanup {
private
static
Test monitor =
new
Test
(
);
public
static
void
main
(
String[] args) {
try
{
InputFile in =
new
InputFile
(
"Cleanup.java"
);
String s;
int
i =
1
;
while
((
s =
in.getLine
(
)) !=
null
)
; // Faire le traitement ligne par ligne ici...
in.dispose
(
);
}
catch
(
Exception e) {
System.err.println
(
"Une exception a été levée dans le main"
);
e.printStackTrace
(
);
}
monitor.expect
(
new
String[] {
"dispose() a réussi"
}
);
}
}
///:~
Le constructeur de la classe InputFile prend un String en argument, correspondant au nom du fichier à ouvrir. Dans un bloc try, il crée un FileReader avec le nom du fichier. Un FileReader n'est pas particulièrement utile à moins que vous ne le détourniez et l'utilisiez pour créer un BufferedReader avec lequel vous pouvez vraiment communiquer - remarquer que l'un des avantages de la classe InputFile est qu'elle combine ces deux actions.
Si le constructeur de la classe FileReader échoue, il lance une FileNotFoundException, qui doit être interceptée séparément. C'est le seul cas dans lequel le fichier ne doit pas être fermé, car il n'a pas été ouvert avec succès. Toutes les autres clauses catch doivent fermer le fichier, car il est ouvert lorsque l'on entre dans ces clauses. (Bien entendu, ça se complique si plus d'une méthode peut lancer une FileNotFoundException. Dans ce cas, il serait peut-être préférable de découper le code en plusieurs blocs try … catch.) La méthode close( ) peut lancer une exception, c'est pourquoi elle est dans un try … catch bien qu'elle soit elle-même dans une clause catch - ce n'est, pour le compilateur Java, qu'une paire de parenthèses en plus. Après avoir exécuté le traitement local, l'exception est relancée, ce qui est approprié, car le constructeur a échoué et que vous ne voudriez pas que la méthode appelante pense que l'objet a bien été créé et est utilisable.
Dans cet exemple, qui n'utilise pas la technique des flags mentionnée précédemment, la clause finally n'est assurément pas le bon endroit pour appeler la méthode close( ) du fichier, car ça refermerait le fichier chaque fois que le constructeur est achevé. Or, nous voulons que le fichier reste ouvert tant que l'objet InputFile est utilisable, ce ne serait donc pas approprié.
La méthode getLine( ) retourne un String contenant la ligne suivante du fichier. Elle appelle la méthode readLine( ), pouvant lancer une exception, mais cette exception est interceptée ce qui fait que getLine( ) ne lance aucune exception. L'un des problèmes de conception avec les exceptions est de savoir si elle doit être gérée totalement à ce niveau, si elle ne doit l'être que partiellement puis être relancée (ou une exception différente), ou si elle doit juste être relancée. La relancer simplement, lorsque c'est approprié, simplifie certainement le code. Dans notre cas, la méthode getLine( ) convertit l'exception en une RuntimeException pour indiquer une erreur de programmation.
La méthode dispose( ) doit être appelée par l'utilisateur quand il a fini d'utiliser l'objet InputFile. Elle permet de libérer les ressources système (comme les handles de fichier) qui sont utilisées par les objets BufferedReader et/ou FileReader. Elle ne doit pas être appelée tant que vous n'avez pas fini d'utiliser l'objet InputFile. Vous pourriez vous dire qu'il serait bon d'appeler une telle méthode dans une méthode finalize( ), mais, comme mentionné dans le chapitre 4, vous ne pouvez pas toujours être certain que la méthode finalize( ) va être appelée (et même si vous savez qu'elle va l'être, vous ne pouvez pas savoir quand). C'est un des inconvénients de Java ; aucun traitement de libération - autres que ceux de la mémoire - n'est effectué automatiquement, c'est pourquoi vous devez informer le programmeur client qu'il est responsable et, si vous le désirez, garantir que la libération est effectuée en utilisant la méthode finalize( ).
Dans Cleanup.java, un objet InputFile est créé et ouvre le fichier source de ce même programme, le fichier est lu ligne par ligne et des numéros de lignes sont ajoutés. Toutes les exceptions sont interceptées de façon générique dans le main( ), bien que vous puissiez choisir une granularité plus fine.
Un des points forts de cet exemple est qu'il montre pourquoi les exceptions sont introduites dans cette partie-là du livre - il y a beaucoup de librairies (comme la librairie I/O citée précédemment) que vous ne pouvez pas utiliser sans avoir des exceptions à gérer. Les exceptions sont tellement partie intégrante de la programmation Java, surtout parce que le compilateur les applique, que vous ne pouvez accomplir que très peu de choses si vous ne savez pas les utiliser.
IX-J. Indication d'exception▲
Quand une exception est lancée, le système de gestion des exceptions teste les blocs catch « les plus proches », dans l'ordre d'écriture. Quand il trouve une correspondance, l'exception est considérée comme traitée et aucune autre recherche n'est lancée.
Identifier une exception ne requiert pas une correspondance parfaite entre l'exception et son gestionnaire. Un objet d'une classe dérivée trouvera un gestionnaire de la classe de base, comme montré dans cet exemple :
//: c09:Human.java
// Hiérarchies de la capture d'exception.
import
com.bruceeckel.simpletest.*;
class
Annoyance extends
Exception {}
class
Sneeze extends
Annoyance {}
public
class
Human {
private
static
Test monitor =
new
Test
(
);
public
static
void
main
(
String[] args) {
try
{
throw
new
Sneeze
(
);
}
catch
(
Sneeze s) {
System.err.println
(
"Caught Sneeze"
);
}
catch
(
Annoyance a) {
System.err.println
(
"Caught Annoyance"
);
}
monitor.expect
(
new
String[] {
"Caught Sneeze"
}
);
}
}
///:~
L'exception Sneeze sera traitée dans le premier bloc catch correspondant, qui est, bien entendu, le premier. Cependant, si vous retirez le premier bloc catch, laissant seulement :
try
{
throw
new
Sneeze
(
);
}
catch
(
Annoyance a) {
System.err.println
(
"Caught Annoyance"
);
}
le code sera toujours fonctionnel puisqu'il traite la classe de base de Sneeze. En d'autres termes, catch(Annoyance e) traitera une Annoyance ou toute autre classe qui en est dérivée. Cela est utile, car si vous décidez d'ajouter plus d'exceptions dérivées à une méthode, alors le code du programmeur client n'aura pas besoin d'être modifié tant que le client traitera les exceptions de la classe de base.
Si vous essayez de « masquer » les exceptions de la classe dérivée en plaçant le bloc catch de la classe de base en premier, comme ceci :
try
{
throw
new
Sneeze
(
);
}
catch
(
Annoyance a) {
System.err.println
(
"Caught Annoyance"
);
}
catch
(
Sneeze s) {
System.err.println
(
"Caught Sneeze"
);
}
le compilateur vous donnera un message d'erreur, car il voit que le bloc catch Sneeze ne peut pas être atteint.
IX-K. Alternative approaches▲
An exception-handling system is a trap door that allows your program to abandon execution of the normal sequence of statements. The trap door is used when an « exceptional condition » occurs, such that normal execution is no longer possible or desirable. Exceptions represent conditions that the current method is unable to handle. The reason exception handling systems were developed is because the approach of dealing with each possible error condition produced by each function call was too onerous, and programmers simply weren't doing it. As a result, they were ignoring the errors. It's worth observing that the issue of programmer convenience in handling errors was a prime motivation for exceptions in the first place.
One of the important guidelines in exception handling is « don't catch an exception unless you know what to do with it. » In fact, one of the important goals of exception handling is to move the error-handling code away from the point where the errors occur. This allows you to focus on what you want to accomplish in one section of your code, and how you're going to deal with problems in a distinct separate section of your code. As a result, your mainline code is not cluttered with error-handling logic, and it's much easier to understand and maintain.
Checked exceptions complicate this scenario a bit, because they force you to add catch clauses in places where you may not be ready to handle an error. This results in the « harmful if swallowed » problem:
try
{
// ... to do something useful
}
catch
(
ObligatoryException e) {}
// Gulp!
Programmers (myself included, in the first edition of this book) would just do the simplest thing, and swallow the exception-often unintentionally, but once you do it, the compiler has been satisfied, so unless you remember to revisit and correct the code, the exception will be lost. The exception happens, but it vanishes completely when swallowed. Because the compiler forces you to write code right away to handle the exception, this seems like the easiest solution even though it's probably the worst thing you can do.
Horrified upon realizing that I had done this, in the second edition I « fixed » the problem by printing the stack trace inside the handler (as is still seen-appropriately-in a number of examples in this chapter). While this is useful to trace the behavior of exceptions, it still indicates that you don't really know what to do with the exception at that point in your code. In this section we'll look at some of the issues and complications arising from checked exceptions, and options that you have when dealing with them.
This topic seems simple. But it is not only complicated, it is also an issue of some volatility. There are people who are staunchly rooted on either side of the fence and who feel that the correct answer (theirs) is blatantly obvious. I believe the reason for one of these positions is the distinct benefit seen in going from a poorly-typed language like pre-ANSI C to a strong, statically-typed language (that is, checked at compile-time) like C++ or Java. When you make that transition (as I did), the benefits are so dramatic that it can seem like strong static type checking is always the best answer to most problems. My hope is to relate a little bit of my own evolution, that has brought the absolute value of strong static type checking into question; clearly, it's very helpful much of the time, but there's a fuzzy line we cross when it begins to get in the way and become a hindrance (one of my favorite quotes is: « All models are wrong. Some are useful. »).
IX-K-1. History▲
Exception handling originated in systems like PL/1 and Mesa, and later appeared in CLU, Smalltalk, Modula-3, Ada, Eiffel, C++, Python, Java, and the postJava languages Ruby and C#. The Java design is similar to C++, except in places where the Java designers felt that the C++ design caused problems.
To provide programmers with a framework that they were more likely to use for error handling and recovery, exception handling was added to C++ rather late in the standardization process, promoted by Bjarne Stroustrup, the language's original author. The model for C++ exceptions came primarily from CLU. However, other languages existed at that time that also supported exception handling: Ada, Smalltalk (both of which had exceptions but no exception specifications) and Modula-3 (which included both exceptions and specifications).
In their seminal paper (44) on the subject, Liskov and Snyder note that a major defect of languages like C that report errors in a transient fashion is that:
« …every invocation must be followed by a conditional test to determine what the outcome was. This requirement leads to programs that are difficult to read, and probably inefficient as well, thus discouraging programmers from signaling and handling exceptions. »
Note that one of the original motivations of exception handling was to prevent this requirement, but with checked exceptions in Java we commonly see exactly this kind of code. They go on to say:
« …requiring that the text of a handler be attached to the invocation that raises the exception would lead to unreadable programs in which expressions were broken up with handlers. »
Following the CLU approach when designing C++ exceptions, Stroustrup stated that the goal was to reduce the amount of code required to recover from errors. I believe that he was observing that programmers were typically not writing error-handling code in C because the amount and placement of such code was daunting and distracting. As a result, they were used to doing it the C way, ignoring errors in code and using debuggers to track down problems. To use exceptions, these C programmers had to be convinced to write « additional » code that they weren't normally writing. Thus, to draw them into a better way of handling errors, the amount of code they would need to « add » must not be onerous. I think it's important to keep this goal in mind when looking at the effects of checked exceptions in Java.
C++ brought an additional idea over from CLU: the exception specification, to programmatically state in the method signature what exceptions may result from calling that method. The exception specification really has two purposes. It can say « I'm originating this exception in my code, you handle it. » But it can also mean « I'm ignoring this exception that can occur as a result of my code, you handle it. » We've been focusing on the « you handle it » part when looking at the mechanics and syntax of exceptions, but here I'm particularly interested in the fact that often we ignore exceptions and that's what the exception specification can state.
In C++ the exception specification is not part of the type information of a function. The only compile-time checking is to ensure that exception specifications are used consistently; for example, if a function or method throws exceptions, then the overloaded or derived versions must also throw those exceptions. Unlike Java, however, no compile-time checking occurs to determine whether or not the function or method will actually throw that exception, or whether the exception specification is complete (that is, whether it accurately describes all exceptions that may be thrown). That validation does happen, but only at run time. If an exception is thrown that violates the exception specification, the C++ program will call the standard library function unexpected( ).
It is interesting to note that, because of the use of templates, exception specifications are not used at all in the standard C++ library. Exception specifications, then, may have a significant impact on the design of Java generics (Java's version of C++ templates, expected to appear in JDK 1.5).
IX-K-2. Perspectives▲
First, it's worth noting that Java effectively invented the checked exception (clearly inspired by C++ exception specifications and the fact that C++ programmers typically don't bother with them). It has been an experiment, which no language since has chosen to duplicate.
Secondly, checked exceptions appear to be an obvious good thing when seen in introductory examples and in small programs. It has been suggested that the subtle difficulties begin to appear when programs start to get large. Of course, largeness usually doesn't happen overnight; it creeps. Languages that may not be suited for large-scale projects are used for small projects that grow, and at some point we realize that things have gone from manageable to difficult. This is what I'm suggesting may be the case with too much type checking; in particular, with checked exceptions.
The scale of the program seems to be a significant issue. This is a problem because most discussions tend to use small programs as demonstrations. One of the C# designers observed that:
« Examination of small programs leads to the conclusion that requiring exception specifications could both enhance developer productivity and enhance code quality, but experience with large software projects suggests a different result-decreased productivity and little or no increase in code quality. » (45)
In reference to uncaught exceptions, the CLU creators stated:
« We felt it was unrealistic to require the programmer to provide handlers in situations where no meaningful action can be taken. » (46)
When explaining why a function declaration with no specification means that it can throw any exception, rather than no exceptions, Stroustrup states:
« However, that would require exception specifications for essentially every function, would be a significant cause for recompilation, and would inhibit cooperation with software written in other languages. This would encourage programmers to subvert the exception-handling mechanisms and to write spurious code to suppress exceptions. It would provide a false sense of security to people who failed to notice the exception. » (47)
We see this very behavior-subverting the exceptions-happening with checked exceptions in Java.
Martin Fowler (author of UML Distilled, Refactoring, and Analysis Patterns) wrote the following to me:
« …on the whole I think that exceptions are good, but Java checked exceptions are more trouble than they are worth. »
I now think that Java's important step was to unify the error reporting model, so that all errors are reported using exceptions. This wasn't happening with C++, because for backward compatibility with C the old model of just ignoring errors was still available. But if you have consistent reporting with exceptions, then the exceptions can be used if desired, and if not, they will propagate out to the highest level (the console or other container program). When Java changed the C++ model so that exceptions were the only way to report errors, the extra enforcement of checked exceptions may have become less necessary.
In the past, I have been a strong believer that both checked exceptions and strong static type checking were essential to robust program development. However, both anecdotal and direct experience (48) with languages that are more dynamic than static have lead me to think that the great benefits actually come from:
- A unified error-reporting model via exceptions, regardless of whether the programmer is forced by the compiler to handle them.
- Type checking, regardless of when it takes place. That is, as long as proper use of a type is enforced, it doesn't matter if it happens at compile time or run time.
On top of this, there are very significant productivity benefits to reducing the compile-time constraints upon the programmer. Indeed, reflection (and eventually, generics) is required to compensate for the over-constraining nature of strong static typing, as you shall see in the next chapter and in a number of examples throughout the book.
I've already been told by some that what I say here constitutes blasphemy, and by uttering these words my reputation will be destroyed, civilizations will fall, and a higher percentage of programming projects will fail. The belief that the compiler can save your project by pointing out errors at compile time runs strong, but it's even more important to realize the limitation of what the compiler is able to do; in Chapter 15, I emphasize the value of an automated build process and unit testing, which give you far more leverage than you get by trying to turn everything into a syntax error. It's worth keeping in mind that:
A good programming language is one that helps programmers write good programs. No programming language will prevent its users from writing bad programs. (49)
In any event, the likelihood of checked exceptions ever being removed from Java seems dim. It would be too radical of a language change, and proponents within Sun appear to be quite strong. Sun has a history and policy of absolute backwards compatibility-to give you a sense of this, virtually all Sun software runs on all Sun hardware, no matter how old. However, if you find that some checked exceptions are getting in your way, or especially if you find yourself being forced to catch exceptions, but you don't know what to do with them, there are some alternatives.
IX-K-3. Passing exceptions to the console▲
In simple programs, like many of those in this book, the easiest way to preserve the exceptions without writing a lot of code is to pass them out of main( ) to the console. For example, if you want to open a file for reading (something you'll learn about in detail in Chapter 12), you must open and close a FileInputStream, which throws exceptions. For a simple program, you can do this (you'll see this approach used in numerous places throughout this book):
//: c09:MainException.java
import
java.io.*;
public
class
MainException {
// Pass all exceptions to the console:
public
static
void
main
(
String[] args) throws
Exception {
// Open the file:
FileInputStream file =
new
FileInputStream
(
"MainException.java"
);
// Use the file ...
// Close the file:
file.close
(
);
}
}
///:~
Note that main( ) is also a method that may have an exception specification, and here the type of exception is Exception,the root class of all checked exceptions. By passing it out to the console, you are relieved from writing try-catch clauses within the body of main( ). (Unfortunately, file I/O is significantly more complex than it would appear to be from this example, so don't get too excited until after you've read Chapter 12).
IX-K-4. Converting checked to unchecked exceptions▲
Throwing an exception from main( ) is convenient when you're writing a main( ), but not generally useful. The real problem is when you are writing an ordinary method body, and you call another method and realize « I have no idea what to do with this exception here, but I don't want to swallow it or print some banal message. » With JDK 1.4 chained exceptions, a new and simple solution prevents itself. You simply « wrap » a checked exception inside a RuntimeException, like this:
try
{
// ... to do something useful
}
catch
(
IDontKnowWhatToDoWithThisCheckedException e) {
throw
new
RuntimeException
(
e);
}
This seems to be an ideal solution if you want to « turn off » the checked exception-you don't swallow it, and you don't have to put it in your method's exception specification, but because of exception chaining you don't lose any information from the original exception.
This technique provides the option to ignore the exception and let it bubble up the call stack without being required to write try-catch clauses and/or exception specifications. However, you may still catch and handle the specific exception by using getCause( ), as seen here:
//: c09:TurnOffChecking.java
// "Turning off" Checked exceptions.
import
com.bruceeckel.simpletest.*;
import
java.io.*;
class
WrapCheckedException {
void
throwRuntimeException
(
int
type) {
try
{
switch
(
type) {
case
0
: throw
new
FileNotFoundException
(
);
case
1
: throw
new
IOException
(
);
case
2
: throw
new
RuntimeException
(
"Where am I?"
);
default
:
return
;
}
}
catch
(
Exception e) {
// Adapt to unchecked:
throw
new
RuntimeException
(
e);
}
}
}
class
SomeOtherException extends
Exception {}
public
class
TurnOffChecking {
private
static
Test monitor =
new
Test
(
);
public
static
void
main
(
String[] args) {
WrapCheckedException wce =
new
WrapCheckedException
(
);
// You can call f() without a try block, and let
// RuntimeExceptions go out of the method:
wce.throwRuntimeException
(
3
);
// Or you can choose to catch exceptions:
for
(
int
i =
0
; i <
4
; i++
)
try
{
if
(
i <
3
)
wce.throwRuntimeException
(
i);
else
throw
new
SomeOtherException
(
);
}
catch
(
SomeOtherException e) {
System.out.println
(
"SomeOtherException: "
+
e);
}
catch
(
RuntimeException re) {
try
{
throw
re.getCause
(
);
}
catch
(
FileNotFoundException e) {
System.out.println
(
"FileNotFoundException: "
+
e);
}
catch
(
IOException e) {
System.out.println
(
"IOException: "
+
e);
}
catch
(
Throwable e) {
System.out.println
(
"Throwable: "
+
e);
}
}
monitor.expect
(
new
String[] {
"FileNotFoundException: "
+
"java.io.FileNotFoundException"
,
"IOException: java.io.IOException"
,
"Throwable: java.lang.RuntimeException: Where am I?"
,
"SomeOtherException: SomeOtherException"
}
);
}
}
///:~
WrapCheckedException.throwRuntimeException( ) contains code that generates different types of exceptions. These are caught and wrapped inside RuntimeException objects, so they become the « cause » of those exceptions.
In TurnOffChecking, you can see that it's possible to call throwRuntimeException( ) with no try block because the method does not throw any checked exceptions. However, when you're ready to catch exceptions, you still have the ability to catch any exception you want by putting your code inside a try block. You start by catching all the exceptions you explicitly know might emerge from the code in your try block-in this case, SomeOtherException is caught first. Lastly, you catch RuntimeException and throw the result of getCause( ) (the wrapped exception). This extracts the originating exceptions, which can then be handled in their own catch clauses.
The technique of wrapping a checked exception in a RuntimeException will be used when appropriate throughout the rest of this book.
IX-L. Conseils pour les exceptions▲
Utilisez les exceptions pour :
- Traiter les problèmes au niveau approprié. (Évitez d'intercepter des exceptions sans savoir quoi faire avec).
- Réparer le problème et rappeler la méthode qui a causé l'exception.
- Colmater et continuer sans réessayer d'appeler la méthode.
- Calculer un résultat alternatif à la place de ce que la méthode était supposée produire.
- Faire tout ce que vous pouvez dans le contexte courant et relancer la même exception vers un contexte supérieur.
- Faire ce que vous pouvez dans le contexte courant et lancer une exception différente vers un contexte supérieur.
- Terminer le programme.
- Simplifier. (Si votre schéma d'exception rend les choses plus complexes, alors c'est pénible et agaçant à manipuler.)
- Rendre votre bibliothèque et votre programme plus surs. (C'est un investissement à court terme pour déboguer, et à long terme pour la robustesse de l'application.)
IX-M. Résumé▲
Améliorer la gestion des erreurs est l'un des meilleurs moyens pour augmenter la robustesse de votre code. La gestion des erreurs est une préoccupation fondamentale pour chaque programme que vous écrivez, mais c'est particulièrement important en Java, où l'un des principaux objectifs est de créer des composants de programme que d'autres pourront utiliser. Pour créer un système robuste, chaque composant se doit d'être robuste. En fournissant un modèle consistant de rapport d'erreur avec des exceptions, Java permet aux composants de communiquer de manière fiable les problèmes de code client.
Les objectifs de la gestion des exceptions en Java sont de simplifier la création de grands programmes fiables en utilisant le moins de code possible et d'avoir plus d'assurance que votre application n'a pas d'erreur non identifiée. Les exceptions ne sont pas terriblement difficiles à apprendre et sont l'une des choses qui procurent des bénéfices immédiats et significatifs à votre projet.
IX-N. Exercices▲
Les solutions aux exercices sélectionnés peuvent être trouvées dans le document électronique The Thinking in Java Annotated Solution Guide, disponible pour un faible coût depuis www.BruceEckel.com.
- Créez une classe avec une méthode main() capable de soulever un objet de la classe Exception à l'intérieur d'un block try. Donnez un argument String au constructeur pour l'instanciation de la classe Exception. Captez l'exception dans la clause catch et affichez l'argument String. Ajoutez une clause finally et affichez un message afin de prouver votre passage ici.
- Créez votre propre classe d'exception en utilisant le mot-clé extends. Écrivez un constructeur pour cette classe qui prend un argument String et qui le stocke à l'intérieur de l'objet avec une référence à String. Écrivez une méthode qui affiche la chaîne stockée. Essayez cette nouvelle exception en créant un block try-catch.
- Écrivez une classe avec une méthode soulevant une exception du type de celle créée à l'exercice 2. Essayez de la compiler sans signification de l'exception pour savoir que dit votre ordinateur. Ajoutez la spécification d'exception appropriée. Essayez votre classe et son exception dans un block try-catch.
- Définissez une référence à un objet et initialisez-la à null. Tentez d'appeler une méthode à travers cette référence. Maintenant, encapsulez ce code dans un block try-catch afin de capter l'exception.
- Créez une classe comportant deux méthodes f( ) et g( ). Dans g( ), soulevez une exception d'un nouveau type que vous définissez. Dans f( ), appelez g( ), attrapez l'exception et, dans une clause catch, déclenchez une exception différente (d'un second type que vous définissez). Testez votre code dans main( ).
- Répétez l'exercice précédent, mais à l'intérieur de la clause catch, encapsulez l'exception de la méthode g( ) dans RuntimeException.
- Créez trois nouveaux types d'exception. Écrivez une classe contenant une méthode qui puisse lever chacun d'eux. Dans main( ), appelez cette méthode, mais n'utilisez qu'une seule clause catch qui captera les trois types d'exception nouvellement créés.
- Écrivez un code qui génère et traite une exception ArrayIndexOutOfBoundsException.
- Créez votre propre comportement de type résurrection en utilisant une boucle while qui se répète tant qu'aucune exception n'est levée.
- Créez une hiérarchie à trois niveaux d'exceptions. Maintenant créez une classe-de-base A avec une méthode qui lance une exception à la base de votre hiérarchie. Héritez B depuis A et outrepassez la méthode afin qu'elle lance une exception au second niveau de votre hiérarchie. Répétez en faisant hériter la classe C de la classe B. Dans main(), créez un C et faites un transtypage ascendant vers A, puis appelez la méthode.
- Démontrez qu'un constructeur d'une classe dérivée ne peut pas capter les exceptions lancées par le constructeur de sa classe de base.
- Montrez que OnOffSwitch.java peut échouer en lançant une RuntimeException dans le bloc try.
- Montrez que WithFinally.java n'échoue pas en lançant une RuntimeException dans le bloc try.
- Modifiez l'Exercice 7 en ajoutant une clause finally. Vérifiez que votre clause finally est exécutée, même si une NullPointerException est lancée.
- Créez un exemple dans lequel vous utilisez un drapeau pour contrôler si le nettoyage du code est appelé, comme décrit dans le second paragraphe après l'en-tête « Constructeurs ».
- Modifiez StormyInning.java en ajoutant un type d'exception UmpireArgument et les méthodes levant cette exception. Testez la hiérarchie ainsi modifiée.
- Supprimez la première clause catch dans Human.java et vérifiez que ce code continue de compiler et de s'exécuter correctement.
- Ajoutez un second niveau de perte d'exception à LostMessage.java afin que HoHumException soit lui-même remplacé par une troisième exception.
- Ajoutez un jeu d'exceptions approriées à c08:GreenhouseControls.java.
- Ajoutez un jeu d'exceptions approriées à c08:Sequence.java.
- Changez la chaîne du nom du fichier dans MainException.java pour désigner un fichier inexistant. Exécutez le programme et notez le résultat.