Penser en Java

2nde édition


précédentsommairesuivant

XII. Gestion des erreurs avec les exceptions

La philosophie de base de Java est que « 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 ils étaient généralement établis 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 [flag], le récepteur avait la charge de tester ces données et de déterminer qu'il y avait eu une anomalie. Malgré cela au fil des ans on se rendit compte que les programmeurs qui utilisent des bibliothèques 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 (44). 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 de prendre la nature primitive de la gestion des erreurs et de renforcer le formalisme. Cette solution à 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 où 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é 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 y a de nombreux exemples qui peuvent être écrits dans ce livre sans apprendre le mécanisme de gestion des exceptions. Ce chapitre vous instruit sur 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.

XII-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. 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 changer de contexte et d'essayer 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, il est intéressant de vérifier que le dénominateur est non nul avant d'effectuer la division. Mais qu'est-ce que cela signifie que le dénominateur soit égal à 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 si c'est 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 instancié de la même façon que tout objet Java est créé : à la volée avec l'instruction new. Ensuite le chemin courant d'exécution (celui que vous ne pouvez 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'exceptions (Exception Handler), dont la tâche est de résoudre le problème de façon à ce que le programme puisse essayer de lancer ou une autre tâche ou juste continuer en séquence.

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'a 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 générer une exception. Cette action ressemble à ceci :

 
Sélectionnez
if(t == null)
  throw new NullPointerException();



Cette chaîne de caractères peut être extraite ultérieurement de différentes façons, nous verrons cela brièvement.

Le mot-clef 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 vous arrivez est complètement différent de celui d'un retour normal d'une méthode. (Vous atterrissez chez le gestionnaire d'exceptions approprié qui peut être très éloigné, plusieurs niveaux plus bas sur la pile d'appels, de l'endroit ou l'exception a été générée.)

De plus, vous pouvez lancer n'importe quel type d'objet Throwable. Ainsi vous générerez une classe d'exceptions pour chaque type d'erreur. L'information à propos de l'erreur est contenue à la fois dans l'objet exception et implicitement dans le type 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é.)

XII-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.

XII-B-1. Le bloc try

Si vous êtes à l'intérieur d'une méthode et que vous générez une exception (où une méthode à l'intérieur de celle-ci génère 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 vos différents appels de méthode ici. Le bloc try est une section ordinaire précédée du mot-clef try.


 
Sélectionnez
try {
  // Code that might generate 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 le 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.

XII-B-2. Les gestionnaires d'exceptions


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-clef catch.

 
Sélectionnez
try {
  // Code that might generate exceptions
} catch(Type1 id1) {
  // Handle exceptions of Type1
} catch(Type2 id2) {
  // Handle exceptions of Type2
} catch(Type3 id3) {
  // Handle exceptions of Type3
}
// etc...



 Chaque clause du catch (gestionnaire d'exceptions) 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'exceptions, 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'exceptions 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 un switch ou vous devez positionner un break après chaque case pour empêcher les autres conditions de s'exécuter.
 Prenez note 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'exceptions.

XII-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 où s'est produite 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'exceptions 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é. 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. Autrement 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 restauratrice 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érait : votre gestionnaire d'exceptions 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.

XII-B-3. Créez vos propres exceptions


 Vous n'êtes pas obligés de vous cantonner aux exceptions Java. Ceci est important, car vous aurez souvent besoin de créer vos exceptions pour distinguer une erreur que votre bibliothèque est capable de générer, mais qui n'a pas été prévue lorsque la hiérarchie des exceptions Java a été pensée.
 Pour créer votre propre classe d'exceptions, vous devez hériter d'un type d'exception déjà existant, de préférence une qui est proche de votre nouvelle exception (parfois ce n'est pas possible). La façon la plus simple de créer une exception est de laisser le compilateur créer le constructeur par défaut, ainsi vous n'avez pas besoin d'écrire de code.

 
Sélectionnez
 //: c10:SimpleExceptionDemo.java
// Inheriting your own exceptions.
class SimpleException  extends Exception {} 
public  class SimpleExceptionDemo {
  public  void f()  throws SimpleException {
    System.out.println(
      "Throwing 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!");
    }
  }
} ///:~


 Quand le compilateur crée le constructeur par défaut, celui-ci automatiquement appelle le constructeur de base par défaut de la classe. Bien sûr vous n'obtenez pas un constructeur SimpleException(String) par défaut, mais ce constructeur n'est guère utilisé. Comme vous le constaterez, la chose la plus importante à propos d'une exception est le nom de sa classe, donc la plupart du temps une exception comme celle décrite au-dessus est satisfaisante.
 Ici le résultat est envoyé vers le flux d'erreur standard de la console en écrivant dans System.err. C'est habituellement un meilleur endroit pour diriger les informations sur les erreurs que System.Out, qui peut être redirigé. Si vous envoyez le flux de sortie vers System.err il ne sera pas redirigé de la même façon que System.out donc l'utilisateur pourra le remarquer plus aisément.
 Créer une classe d'exceptions dont le constructeur a aussi un constructeur qui prend un argument de type string est assez simple :

 
Sélectionnez
//: c10:FullConstructors.java
// Inheriting your own exceptions.
class MyException  extends Exception {
  public MyException() {}
  public MyException(String msg) {
    super(msg);
  }
}
public  class FullConstructors {
  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(System.err);
    }
    try {
      g();
    } catch(MyException e) {
      e.printStackTrace(System.err);
    }
  }
} ///:~


 Le code ajouté est minime, deux constructeurs qui définissent la façon dont MyException est créé. Dans le second constructeur, le constructeur de base avec un argument de type String est appelé de façon explicite avec le mot-clef super.
 L'information de la pile des appels est redirigée vers System.err ainsi on notera plus simplement dans l'événement que system.out a été redirigé.
 La sortie du programme est :

 
Sélectionnez
Throwing MyException from f()
MyException
        at FullConstructors.f(FullConstructors.java:16)
        at FullConstructors.main(FullConstructors.java:24)
Throwing MyException from g()
MyException: Originated in g()
        at FullConstructors.g(FullConstructors.java:20)
        at FullConstructors.main(FullConstructors.java:29)


 Vous pouvez noter l'absence de message de détail dans le MyException généré depuis f().
 La création d'exceptions personnalisées peut-être plus poussée. Vous pouvez ajouter des constructeurs et des membres supplémentaires.

 
Sélectionnez
//: c10:ExtraFeatures.java
// Further embellishment of exception classes.
class MyException2  extends Exception {
  public MyException2() {}
  public MyException2(String msg) {
    super(msg);
  }
  public MyException2(String msg,  int x) {
    super(msg);
    i = x;
  }
  public  int val() { return i; }
  private int i;
}
public  class ExtraFeatures {
  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(System.err);
    }
    try {
      g();
    } catch(MyException2 e) {
      e.printStackTrace(System.err);
    }
    try {
      h();
    } catch(MyException2 e) {
      e.printStackTrace(System.err);
      System.err.println( "e.val() = " + e.val());
    }
  }
} ///:~


 Un membre i a été ajouté, avec une méthode qui lit cette donnée et un constructeur supplémentaire qui l'initialise. Voici la sortie :

 
Sélectionnez
Throwing MyException2 from f()
MyException2
        at ExtraFeatures.f(ExtraFeatures.java:22)
        at ExtraFeatures.main(ExtraFeatures.java:34)
Throwing MyException2 from g()
MyException2: Originated in g()
        at ExtraFeatures.g(ExtraFeatures.java:26)
        at ExtraFeatures.main(ExtraFeatures.java:39)
Throwing MyException2 from h()
MyException2: Originated in h()
        at ExtraFeatures.h(ExtraFeatures.java:30)
        at ExtraFeatures.main(ExtraFeatures.java:44)
e.val() = 47



 Puisqu'une exception est juste une sorte d'objet vous pouvez continuer à enrichir vos classes d'exceptions, mais gardez à l'esprit que toutes ces améliorations peuvent être ignorées par les clients programmeurs utilisant vos paquetages puisqu'ils vont probablement regarder quelle exception pourra être générée et rien de plus. (C'est de cette façon que sont utilisées le plus souvent les bibliothèques d'exceptions Java.)

XII-C. Spécifier des exceptions


 En java vous êtes obligé de fournir au programmeur qui appelle vos méthodes la liste des exceptions pouvant être générées. C'est correct, en effet le programmeur peut ainsi exactement savoir quel code écrire pour attraper toute exception potentielle. Bien sûr si le code source est disponible le programmeur pourra y jeter un Sil et rechercher le mot-clef 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) qui vous permet d'informer formellement le programmeur client quelles exceptions sont générées par cette méthode, pour que le programmeur 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-clef throws suivi par la liste des types d'exception possibles. Vos définitions de méthodes pourront ressembler à ceci :

 
Sélectionnez
void f()  throws TooBig, TooSmall, DivZero {  //...



 Si vous écrivez

 
Sélectionnez
void f() {  // ...



 Cela signifie qu'aucune exception ne pourra être générée par la méthode. (Excepté les exceptions de type RuntimeException, qui peuvent être générées n'importe où nous verrons cela plus tard.)
 Vous ne pouvez pas mentir à propos de 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 ou indiquer en spécifiant que cette exception peut être générée dans votre méthode. En forçant la spécification des exceptions de haut en bas, Java garantit que la correction des exceptions est assurée au moment de la compilation.
 Il y a un endroit ou vous pouvez tricher : vous déclarez générer une exception que vous ne n'allez pas générer. Le compilateur vous prend au pied de la lettre, et force l'utilisateur de la méthode à agir comme si elle pouvait générer l'exception. Ceci à l'effet positif d'être un réceptacle pour cette exception, ainsi vous pourrez générer 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 puissent générer des exceptions.

XII-C-1. 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'exceptions Exception. (Il y a d'autres types d'exception de base, mais Exception est celle qui est pertinente pour tous les types d'activités de programmation).

 
Sélectionnez
catch(Exception e) {
  System.err.println("Caught an exception");
}



 Ce code interceptera toutes les exceptions donc si vous l'utilisez, vous voudrez le placer à la fin de votre liste de gestionnaires pour éviter la préemption sur certains gestionnaires qui pourraient dans l'autre cas le suivre.
 Comme la classe Exception est la base de toutes les classes d'exception qui sont utiles au programmeur, vous n'avez pas beaucoup d'informations spécifiques sur l'exception, mais vous pouvez utiliser les méthodes venant de son type de base Throwable :
String getMessage() String getLocalizedMessage() donne le message détaillé ou un message adapté à cette localisation particulière ;
String toString() retourne une courte description de l'objet Throwable, incluant le message détaillé s'il y en a un ;
void printStackTrace() void printStackTrace(PrintStream) void printStackTrace(PrintWriter) imprime l'objet throwable ainsi que la trace de la pile des appels de l'objet Throwable. La pile d'appels vous montre la séquence d'appels de méthodes qui vous ont conduit à l'endroit où l'exception a été levée. La première version imprime les informations vers la sortie standard la seconde et la troisième vers un flux de votre choix. Dans le titre XIII, vous comprendrez pourquoi il y a deux types de flux ;
Throwable fillInStackTrace() enregistre des informations liées à cet objet Throwable concernant l'état de la « stack frame ». Utile quand une application relance une exception (nous approfondirons cela plus loin).
 En plus, vous pouvez utiliser des méthodes provenant de la classe mère de Throwable qui est objet (ancêtre de tout type de base). Celle qui pourrait être utile pour les exceptions est getClass() qui retourne un objet représentant la classe de cet objet. Vous pouvez interroger cet objet de Class avec les méthodes getName() ou toString(). Vous pouvez aussi faire des choses plus sophistiquées avec l'objet Class qui ne sont pas nécessaires dans la gestion des exceptions. Les objets Class seront étudiés plus loin dans ce livre.
 Voici un exemple qui montre un emploi simple de la méthode d'Exception de base :

 
Sélectionnez
//: c10:ExceptionMethods.java
// Demonstrating the Exception Methods.
public  class ExceptionMethods {
  public  static  void main(String[] args) {
    try {
      throw  new Exception( "Here's my Exception");
    } catch(Exception e) {
      System.err.println("Caught Exception");
      System.err.println(
        "e.getMessage(): " + e.getMessage());
      System.err.println(
        "e.getLocalizedMessage(): " +
         e.getLocalizedMessage());
      System.err.println("e.toString(): " + e);
      System.err.println( "e.printStackTrace():");
      e.printStackTrace(System.err);
    }
  }
} ///:~



 La sortie du programme est :

 
Sélectionnez
Caught Exception
e.getMessage(): Here's my Exception
e.getLocalizedMessage(): Here's my Exception
e.toString(): java.lang.Exception: 
   Here's my Exception
e.printStackTrace():
java.lang.Exception: Here's my Exception
at ExceptionMethods.main(ExceptionMethods.java:7)
java.lang.Exception: 
   Here's my Exception
at ExceptionMethods.main(ExceptionMethods.java:7)



 Vous pouvez observer que les méthodes donnent successivement plus d'informations, chacune est effectivement une évolution de la précédente.

XII-C-2. Relancer une exception


 Quelques fois vous voudrez relancer une exception que vous venez d'intercepter, particulièrement quand vous utilisez la classe Exception pour attraper n'importe quelle exception. Comme vous avez déjà la référence de l'exception courante, vous pouvez tout simplement la relancer.

 
Sélectionnez
catch(Exception e) {
  System.err.println( "An exception was thrown");
  throw e;
}



 La redirection de l'exception envoie l'exception au gestionnaire d'exceptions de contexte supérieur le plus proche. Toute autre clause catch placée après dans le même bloc try est ignorée. De plus, toute information concernant l'objet exception est préservée, donc le gestionnaire d'exceptions du niveau supérieur qui peut attraper ce type d'exception spécifique peut extraire toutes les informations de cet objet.
 Si vous redirigez simplement l'exception courante, l'information que vous restituerez fera référence aux origines de l'exception et non de l'endroit ou vous la redirigez. Si vous voulez placer de nouvelles informations dans la pile des appels, vous pouvez le faire en appelant la méthode fillInStackTrace(), qui retourne un objet exception qu'elle a créé en ajoutant les informations actuelles de la pile dans l'ancien objet exception. Voyons à quoi cela ressemble.

 
Sélectionnez
//: c10:Rethrowing.java
// Demonstrating fillInStackTrace()
public  class Rethrowing {
  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(System.err);
      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(System.err);
    }
  }
} ///:~


 Les numéros des lignes importantes sont marqués dans les commentaires, avec la ligne 17 non mise en commentaire (cf. le code ci-dessus) l'exécution produit comme sortie :

 
Sélectionnez
originating the exception in f()
Inside g(), e.printStackTrace()
java.lang.Exception: thrown from f()
        at Rethrowing.f(Rethrowing.java:8)
        at Rethrowing.g(Rethrowing.java:12)
        at Rethrowing.main(Rethrowing.java:24)
Caught in main, e.printStackTrace()
java.lang.Exception: thrown from f()
        at Rethrowing.f(Rethrowing.java:8)
        at Rethrowing.g(Rethrowing.java:12)
        at Rethrowing.main(Rethrowing.java:24)


 Ainsi la trace de la pile d'exceptions se souvient toujours de son vrai point d'origine, sans tenir compte du nombre de fois qu'il sera relancé.
 Avec la ligne 17 mise en commentaire et la ligne 18 décommentée fillInStackTrace() est utilisée ce qui donne le résultat suivant :

 
Sélectionnez
originating the exception in f()
Inside g(), e.printStackTrace()
java.lang.Exception: thrown from f()
        at Rethrowing.f(Rethrowing.java:8)
        at Rethrowing.g(Rethrowing.java:12)
        at Rethrowing.main(Rethrowing.java:24)
Caught in main, e.printStackTrace()
java.lang.Exception: thrown from f()
        at Rethrowing.g(Rethrowing.java:18)
        at Rethrowing.main(Rethrowing.java:24)



 Grâce à fillInStackTrace() la ligne 18 devient l'origine de l'exception.
 La classe Throwable doit apparaître dans la spécification d'exceptions pour g() et main() parce que fillInStackTrace() produit une référence à un objet Throwable. Comme Throwable est la classe de base de toute Exception il est possible d'avoir un objet de type Throwable qui ne soit pas une Exception et donc que le gestionnaire d'exceptions du main() ne le traite pas. Pour être sûr que tout soit correct, le compilateur force une spécification d'exception pour Throwable. Par exemple l'exception dans le programme suivant n'est pas interceptée dans main() :

 
Sélectionnez
//: c10:ThrowOut.java
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 rediriger une exception différente de celle que vous avez interceptée. En procédant ainsi vous obtenez un effet similaire a l'utilisation de fillInStackTrace(). L'information sur l'emplacement originel de l'exception est perdue, et l'information qu'il vous reste est celle relative au nouveau throw() :

 
Sélectionnez
//: c10:RethrowNew.java
// Rethrow a different object 
// from the one that was caught.
class OneException  extends Exception {
  public OneException(String s) {  super(s); }
}
class TwoException  extends Exception {
  public TwoException(String s) {  super(s); }
}
public  class RethrowNew {
  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(System.err);
      throw  new TwoException( "from main()");
    }
  }
} ///:~


 Le résultat de l'exécution est :

 
Sélectionnez
originating the exception in f()
Caught in main, e.printStackTrace()
OneException: thrown from f()
        at RethrowNew.f(RethrowNew.java:17)
        at RethrowNew.main(RethrowNew.java:22)
Exception in thread  "main" TwoException: from main()
        at RethrowNew.main(RethrowNew.java:27)


 L'exception finale sait seulement qu'elle vient de main() et non de f().
 Vous n'avez pas à vous soucier de la destruction des exceptions précédentes. Ce sont tous des objets créés avec l'instruction new(), le garbage collector les détruit donc automatiquement.

XII-D. Les exceptions Java standard


 La classe Java Throwable décrit tout ce qui peut être généré comme exception. Il y a deux sortes d'objets Throwable (« Sortes de » = « Héritées de »). Error représente les erreurs système et de compilation dont vous n'avez pas à vous soucier (excepté quelques cas spécifiques). Exception est le type de base qui peut être généré à partir de n'importe quelle méthode de classe de bibliothèque Java standard. Les Exceptions sont donc le type de base qui intéresse le programmeur Java.
 La meilleure façon d'avoir un aperçu des exceptions est de lire la documentation HTML de Java qui est disponible à java.sun.com. Il est utile de le faire juste pour s'apercevoir de la variété des exceptions, mais vous verrez rapidement que rien à part le nom ne distingue une exception d'une autre. Comme le nombre d'exceptions Java continue de s'accroître, il est inutile d'en imprimer la liste. Chaque nouvelle bibliothèque que vous achèterez à un éditeur aura probablement ses propres exceptions. Le point important à comprendre est le concept et ce que vous devez faire avec les exceptions.
 Le principe de base est que le nom de l'exception représente le problème qui est apparu, et le nom de l'exception est censé être relativement explicite. Toutes les exceptions ne sont pas définies dans java.lang ; certaines ont été créées pour être utilisées dans d'autres bibliothèques telles que util, net et io ce que l'on peut voir à partir du nom complet des classes dont elles ont hérité. Ainsi toutes les exceptions I/O (Entrée/Sortie) héritent de java.io.IOException.

XII-D-1. Le cas particulier RuntimeException


 Le premier exemple du chapitre était :

 
Sélectionnez
if(t ==  null)
  throw  new NullPointerException(); 

Cela peut paraître effrayant de penser que l'on devra vérifier le null pour chaque référence passée à une méthode (puisqu'on ne sait pas si l'appelant a passé une référence valide). Heureusement vous n'avez pas à le faire. C'est une des tâches standard de run-time checking que Java exécute pour vous, et si un appel est fait à une référence contenant la valeur null, Java va automatiquement générer une NullPointerException. Donc le code ci-dessus est superflu.
  Il y a tout un groupe d'exceptions qui sont dans cette catégorie. Elles sont générées automatiquement par Java et vous n'avez pas besoin de les inclure dans vos spécifications d'exception. Elles sont regroupées dans une seule classe de base nommée RuntimeExceptions, qui est un parfait exemple de l'héritage : cela établit une famille de types qui ont des caractéristiques et des comportements communs. Aussi vous n'avez jamais besoin de spécifier qu'une méthode peut générer une RuntimeException puisque c'est géré. Puisque cela indique la présence de bogues vous n'interceptez jamais de RuntimeException c'est géré automatiquement. Si vous étiez obligés de tester les RuntimeException votre code pourrait devenir illisible. Bien que vous n'interceptiez pas de RuntimeException dans vos propres paquetages, vous pourriez décider de générer des RuntimeException.
  Que se passe-t-il quand vous n'interceptez pas de telles exceptions ? Comme le compilateur ne demande pas de spécifications d'exceptions pour celles-ci, il est probable qu'une RuntimeException puisse remonter jusqu'à votre méthode main() sans être interceptée. Pour voir ce qui se passe dans ce cas-là, essayez l'exemple suivant :

 
Sélectionnez
//: c10:NeverCaught.java
// Ignoring RuntimeExceptions.
public  class NeverCaught {
  static  void f() {
    throw  new RuntimeException( "From f()");
  }
  static  void g() {
    f();
  }
  public  static  void main(String[] args) {
    g();
  }
} ///:~



  vous pouvez déjà voir qu'une RuntimeException (ou tout ce qui en hérite) est un cas particulier, puisque le compilateur ne demande pas de spécifications pour ce type.
  La trace est :

 
Sélectionnez
Exception in thread "main"
java.lang.RuntimeException: From f()
        at NeverCaught.f(NeverCaught.java:9)
        at NeverCaught.g(NeverCaught.java:12)
        at NeverCaught.main(NeverCaught.java:15)



  La réponse est donc : si une RuntimeException arrive jusqu'à la méthode main() sans être interceptée, PrintStackTrace() est appelée pour cette exception à la sortie du programme.
  Gardez à l'esprit que vous pouvez ignorer seulement les RuntimeException dans vos développements puisque la gestion de toutes les autres exceptions est renforcée par le compilateur. Le raisonnement est qu'une RuntimeRxception représente une erreur de programmation.

  1. Une erreur que vous ne pouvez pas intercepter(recevoir une référence transmise par un programme client avec la valeur null par exemple).
  2. Une erreur que vous auriez dû prévoir dans votre code (comme ArrayIndexOutOfBoundsException où vous auriez dû faire attention à la taille du tableau).


  Vous pouvez voir quel bénéfice apportent les exceptions dans ce cas, puisque cela aide le processus de débogage.
  Il est intéressant de noter que vous ne pouvez pas ranger la gestion des exceptions Java dans un outil à utilisation unique. Oui c'est fait pour gérer ces affreuses run-time error qui vont apparaître à cause de forces hors de contrôle de votre code, mais c'est aussi essentiel pour certains types de bogues de développement que le compilateur ne peut détecter.

XII-E. Faire le ménage avec finally


  Il y a souvent un morceau de code que vous voulez exécuter qu'une exception ou pas soit générée au cœur d'un bloc try. C'est généralement destiné à des opérations autres que le rafraîchissement de la mémoire (puisque le garbage collector s'en occupe). Pour faire ceci il faut utiliser finally à la fin de chaque gestionnaire d'exceptions. L'image complète d'un paragraphe gestion d'exceptions est :

 
Sélectionnez
try {
  // The guarded region: Dangerous activities
  // that might throw A, B, or C 
} catch(A a1) {
  // Handler for situation A
} catch(B b1) {
  // Handler for situation B
} catch(C c1) {
  // Handler for situation C
} finally {
  // Activities that happen every time
}



  Pour démontrer que la clause finally est toujours exécutée, lancer ce programme :

 
Sélectionnez
//: c10:FinallyWorks.java
// The finally clause is always executed.
class ThreeException  extends Exception {}
public  class FinallyWorks {
  static  int count = 0;
  public  static  void main(String[] args) {
    while(true) {
      try {
        // Post-increment is zero first time:
        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; // out of "while"
      }
    }
  }
} ///:~



  Ce programme vous donne aussi une aide pour comprendre comment gérer le fait que Java (comme les exceptions en C++) ne vous permet de reprendre l'exécution de l'endroit où l'exception a été générée, comme vu 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 périphériques afin de permettre à la boucle différentes approches avant d'abandonner. De cette façon vous pouvez obtenir un plus grand niveau de robustesse dans vos programmes.
  La sortie est :

 
Sélectionnez
ThreeException
In finally clause
No exception
In finally clause



  Qu'une exception soit générée ou pas, la clause finally est exécutée.

XII-E-1. À quoi sert le finally ?


  Dans un langage sans ramasse-miettes [Garbage Collector] et sans appel automatique aux destructeurs, 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. Mais Java a un ramasse-miettes. Donc il n'y a aucun destructeur à appeler. Donc quand avez-vous besoin de la clause finally en Java ?
Finally est nécessaire quand vous avez besoin de remettre à l'état original autre chose que le mémoire. C'est une sorte de nettoyage comme un fichier ouvert une connexion réseau quelque chose affiché à l'écran ou un switch dans le monde extérieur comme modélisé dans l'exemple suivant :

 
Sélectionnez
//: c10:OnOffSwitch.java 
// Why use finally?
class Switch {
  boolean state =  false;
  boolean read() {  return state; }
  void on() { state =  true; }
  void off() { state =  false; }
} 
class OnOffException1  extends Exception {}
class OnOffException2  extends Exception {}
public  class OnOffSwitch {
  static Switch sw =  new Switch();
  static  void f() throws 
    OnOffException1, OnOffException2 {}
  public  static  void main(String[] args) {
    try {
      sw.on();
      // Code that can throw 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() se termine donc sw.off() est placée à la fin de main() et à la fin de chaque gestionnaire d'exceptions, mais il se peut qu'une exception soit générée et qu'elle ne soit pas interceptée ici. Vous pouvez placer le code de nettoyage dans un seul endroit le bloc finally :

 
Sélectionnez
//: c10:WithFinally.java
// Finally Guarantees cleanup.
public  class WithFinally {
  static Switch sw =  new Switch();
  public  static  void main(String[] args) {
    try {
      sw.on();
      // Code that can throw 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ée à un endroit unique où on est sûr qu'elle sera exécutée 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'exceptions recherche un gestionnaire de plus haut niveau.

 
Sélectionnez
//: c10:AlwaysFinally.java
// Finally is always executed.
class FourException  extends Exception {}
public  class AlwaysFinally {
  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");
    }
  }
} ///:~



  La sortie de ce programme vous montre ce qui arrive :

 
Sélectionnez
Entering first try block
Entering second try block
finally in 2nd  try block
Caught FourException in 1st try block
finally in 1st  try block



  Le mot-clef finally sera aussi exécuté dans des situations ou les mots-clefs break et continue sont impliqués. Notez qu'avec break continue et finally il n'y a pas besoin de goto en Java.

XII-E-2. Le défaut : l'exception perdue


  En général l'implémentation de Java est de grande qualité, mais malheureusement, il y a un petit défaut. Bien qu'une exception soit une indication de crise dans votre programme et ne doive jamais être ignorée, il est possible qu'une exception soit perdue. Cela est possible dans une certaine configuration de la clause finally :

 
Sélectionnez
//: c10:LostMessage.java
// How an exception can be lost.
class VeryImportantException  extends Exception {
  public String toString() {
    return  "A very important exception!";
  }
}
class HoHumException  extends Exception {
  public String toString() {
    return  "A trivial exception";
  }
}
public  class LostMessage {
  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();
    }
  }
} ///:~



  Le résultat est :

 
Sélectionnez
Exception in thread  "main" A trivial exception
    at LostMessage.dispose(LostMessage.java:21)
    at LostMessage.main(LostMessage.java:29)



  Vous pouvez voir qu'il ne reste aucune trace de VeryImportantException qui est tout simplement remplacée par HoHumException dans la clause finally. Ceci est une faille sérieuse puisqu'elle signifie qu'une exception peut être complètement perdue et bien plus difficile et subtile à détecter que les autres. C++ lui au contraire traite la situation en considérant qu'une seconde exception générée avant la première est une erreur de programmation. Peut-être qu'une future version de Java corrigera ce défaut (autrement vous pouvez exclure du bloc try-catch toute méthode susceptible de générer une exception telle que dispose()).

XII-F. Restriction 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 classe de base de la méthode fonctionnera avec tous les objets dérivant de cette classe (un concept fondamental de la POO), incluant les exceptions.
  Cet exemple démontre les restrictions imposées (à la compilation) pour les exceptions :

 
Sélectionnez
//: c10:StormyInning.java
// Overridden methods may throw only the 
// exceptions specified in their base-class 
// versions, or exceptions derived from the 
// base-class exceptions.
class BaseballException  extends Exception {}
class Foul  extends BaseballException {}
class Strike  extends BaseballException {}
abstract  class Inning {
  Inning() throws BaseballException {}
  void event ()  throws BaseballException {
   // Doesn't actually have to throw anything
  }
  abstract  void atBat()  throws Strike, Foul;
  void walk() {} // Throws nothing
}
class StormException  extends Exception {}
class RainedOut  extends StormException {}
class PopFoul  extends Foul {}
interface Storm {
  void event()  throws RainedOut;
  void rainHard()  throws RainedOut;
}
public  class StormyInning  extends Inning 
    implements Storm {
  // OK to add new exceptions for 
  // constructors, but you must deal 
  // with the base constructor exceptions:
  StormyInning() throws RainedOut, 
    BaseballException {}
  StormyInning(String s) throws Foul, 
    BaseballException {}
 // Regular methods must conform to base class:
 //! void walk() throws PopFoul {} //Compile error
 // Interface CANNOT add exceptions to existing
  // methods from the base class:
//! public void event() throws RainedOut {}
  // If the method doesn't already exist in the
  // base class, the exception is OK:
  public  void rainHard()  throws RainedOut {}
 // You can choose to not throw any exceptions,
  // even if base version does:
  public  void event() {}
  // Overridden methods can throw 
  // inherited exceptions:
  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 error");
    }
    // Strike not thrown in derived version.
    try {
      // What happens if you upcast?
      Inning i = new StormyInning();
      i.atBat();
      // You must catch the exceptions from the
      // base-class version of the method:
    } 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 lancer une exception alors qu'elles ne le feront jamais. C'est tout à fait légitime puisqu'elles vous laissent la possibilité de forcer l'utilisateur à attraper des exceptions dans des versions surchargées de event(). Le même principe s'applique pour les méthodes abstraites [abstract].
  L'interface Storm est intéressante parce qu'elle contient une méthode 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 vous regardez StormyInning extends Inning implements Storm, vous voyez que la méthode event() de Storm ne peut pas changer l'interface de la méthode event() de Inning. Une fois encore cela est sensé, car lorsque vous travaillez avec la classe de base vous ne sauriez pas si vous attrapez la bonne chose. Bien sûr si une méthode décrite dans l'interface n'est pas dans la classe de base telle que Rainhard(), ainsi 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 le constructeur de base. Malgré tout ensuite, un constructeur de classe de base doit bien être appelé à un moment ou à un autre (ici le constructeur par défaut est appelé automatiquement) le constructeur de la classe dérivée doit déclarer toutes les exceptions du constructeur de base dans ses spécifications d'exception. Un constructeur de classe dérivée ne peut pas intercepter les exceptions 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, 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 de 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 par la version de base de atBat(). De cette façon si quelqu'un écrit du code qui fonctionne avec Inning() et qui appelle atBat() doit intercepter les exceptions Foul. Puisque PopFoul hérite de Foul le gestionnaire d'exceptions interceptera PopFoul.
  Le dernier point intéressant est main() Ici vous pouvez voir que si vous avez à faire à un objet StormyInning le compilateur vous oblige à n'intercepter que les exceptions qui sont spécifiques à la classe, mais si vous castez l'objet, 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.
  Il est utile de réaliser que bien que la spécification des exceptions soit renforcée au moment de l'héritage par le compilateur, la spécification des exceptions n'est pas une partie d'un type d'une méthode, qui est composée seulement d'un nom de méthode et d'un type d'arguments, vous ne pouvez pas surcharger une méthode basée sur une spécification d'exception. 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 fille. Autrement dit l'interface de spécification des exceptions d'une classe ne peut que se restreindre alors que c'est le contraire pour l'interface de la classe durant l'héritage.

XII-G. Les constructeurs


  Quand on écrit du code avec des spécifications d'exceptions il y a une question particulière que l'on doit se poser « si une exception se produit est-ce que la situation sera rétablie à l'état initial ? » dans la plupart des cas, vous êtes sauvés, mais dans celui des constructeurs il y a un problème. Le constructeur place l'objet dans un état de démarrage sain, mais peut accomplir certaines opérations - telles qu'ouvrir un fichier - qui ne seront pas terminées tant que l'utilisateur n'aura pas terminé de travailler avec l'objet et appelé une certaine méthode de restauration. Si vous générez une exception dans le constructeur, ces restaurations peuvent ne pas s'effectuer de façon appropriée. Cela signifie que vous devez être extrêmement prudent quand vous écrivez votre propre constructeur.
  Comme vous venez d'apprendre le mot-clef finally vous pouvez penser que c'est la solution, mais ce n'est pas si simple, parce que Finally fait le nettoyage du code à chaque exécution même dans les cas où vous ne le souhaiteriez pas. Aussi si vous faites la restauration dans le finally vous devez placer un indicateur qui vous indiquera de ne rien faire dans le finally si le constructeur s'est bien déroulé, mais cela n'est pas particulièrement élégant (vous liez votre code d'un endroit à un autre) c'est mieux de ne pas faire ce genre d'opérations dans le finally à moins d'y être forcé.
  Dans l'exemple suivant, une classe appelée InputFile est créée, elle vous permet d'ouvrir un fichier et d'en lire une ligne (convertie en String) à la fois. Elle utilise les classes FileReader et InputBuffer de la bibliothèque I/O standard de Java dont nous discuterons au titre XIII, mais elles sont assez simples donc vous n'aurez probablement pas de problème à comprendre leur comportement de base :

 
Sélectionnez
//: c10:Cleanup.java
// Paying attention to exceptions
// in constructors.
import java.io.*;
class InputFile {
  private BufferedReader in;
  InputFile(String fname) throws Exception {
    try {
      in = 
        new BufferedReader(
          new FileReader(fname));
      // Other code that might throw exceptions
    } catch(FileNotFoundException e) {
      System.err.println(
        "Could not open " + fname);
      // Wasn't open, so don't close it
      throw e;
    } catch(Exception e) {
      // All other exceptions must close it
      try {
        in.close();
      } catch(IOException e2) {
        System.err.println(
          "in.close() unsuccessful");
      }
      throw e; // Rethrow
    } finally {
      // Don't close it here!!!
    }
  }
  String getLine() {
    String s;
    try {
      s = in.readLine();
    } catch(IOException e) {
      System.err.println(
        "readLine() unsuccessful");
      s = "failed";
    }
    return s;
  }
  void cleanup() {
    try {
      in.close();
    } catch(IOException e2) {
      System.err.println(
        "in.close() unsuccessful");
    }
  }
}
public  class Cleanup {
  public  static  void main(String[] args) {
    try {
      InputFile in = 
        new InputFile( "Cleanup.java");
      String s;
      int i = 1;
      while((s = in.getLine()) !=  null)
        System.out.println(""+ i++ + color= ": " + s);
      in.cleanup();
    } catch(Exception e) {
      System.err.println(
        "Caught in main, e.printStackTrace()");
      e.printStackTrace(System.err);
    }
  }
} ///:~


  Le constructeur d'InputFile prend une String comme paramètre, qui contient le nom du fichier que vous voulez ouvrir, à l'intérieur du bloc try il crée un FileReader. Un FileReader n'est pas particulièrement utile à moins que vous ne le détourniez et l'utilisiez pour créer un BuffereReader avec lequel vous puissiez communiquer, un des bénéfices de l'InputFile est qu'il combine ces deux actions.
  Si le constructeur de FileReader échoue, il génère une FileNotFoundException qui doit être interceptée séparément puisque dans ce cas-là, vous ne voulez pas fermer le fichier qui n'a pas été ouvert. Toute autre clause catch doit fermer le fichier puisqu'il a été ouvert (bien sûr c'est plus délicat plus d'une méthode peut générer une FileNotfoundException dans ce cas vous pouvez vouloir séparer cela dans plusieurs blocs try). La méthode close() doit lancer une exception ce qui est donc essayé et capté bien que cela soit dans le bloc d'une autre clause catch - c'est juste une autre paire d'accolades pour le compilateur Java. Après l'exécution des opérations locales, l'exception est relancée, ce qui est adéquat puisque le constructeur a échoué, et vous ne voudriez pas que la méthode appelée ait à supposer que l'objet a été correctement créé et soit valide.
  Dans cet exemple, qui n'utilise pas la technique du drapeau mentionnée précédemment, la cause finally n'est sûrement pas la place pour close() (NdT fermer) le fichier, puisque cela fermerait le fichier à chaque finalisation du constructeur. Comme nous désirons que le fichier soit ouvert tout le temps de la durée de vie de l'objet InputFile cela ne serait pas approprié.
  La méthode getLine() renvoie un String contenant la prochaine ligne du fichier. Elle appelle readLine(), qui peut lancer une exception, mais cette exception est provoqué donc getLine() ne lance pas d'exception. Une des finalités de la conception des exceptions est soit de porter complètement une exception à ce niveau, de la porter partiellement et de passer la même exception (ou une différente), ou soit de la passer tout simplement. La passer, quand c'est approprié, peut certainement simplifier le codage. La méthode getLine() devient :

 
Sélectionnez
String getLine()  throws IOException {
  return in.readLine();
}



  Mais bien sûr, l'appelant a maintenant la responsabilité de porter toute IOException qui puisse apparaître.
  La méthode cleanUp() doit être appelée par l'utilisateur lorsqu'il a fini d'utiliser l'objet InputFile(). Ceci relâchera les ressources système (comme les poignées de file) qui étaient utilisées par les objets BufferedReader ou/et FileReader. Vous ne voudrez pas faire cela tant que vous n'aurez pas fini avec l'objet InputFile, au point de le laisser partir. Vous devrez penser à mettre une fonctionnalité de ce type dans une méthode finaliste(), mais comme il est mentionné dans le titre VI, vous ne pouvez pas toujours être sûr que finaliste() sera appelé (même si vous pouvez être sûr de son appel, vous ne savez pas quand). C'est un des à-cotés de Java : tout nettoyage - autre que le nettoyage de la mémoire - ne se lance pas automatiquement, donc vous devez informer le programmeur client qu'il est responsable, et garantir au mieux que possible que le nettoyage s'exécute par l'usage de finalise().
  Dans Cleanup.java, un InputFile est créé pour ouvrir le même fichier source qui crée le programme, le fichier est lu une ligne à la fois, et les numéros de ligne sont ajoutés. Toutes les exceptions sont causées génériquement dans main(), si bien que vous pouvez choisir une plus grande finesse.
  Un des bénéfices de cet exemple est de vous montrer pourquoi les exceptions sont abordées à ce point du livre - vous ne pouvez pas faire de l'E/S [I/O] sans utiliser les exceptions. Les exceptions sont tellement intégrées à la programmation en Java, spécialement parce que le compilateur les demande, que vous pouvez accomplir tellement peu de choses sans savoir comment travailler avec elles.

XII-H. Indication d'exception


  Quand une exception est lancée, le système d'indication des exceptions regarde dans les « plus proches » identifiants dans l'ordre de leur écriture. Quand il trouve une correspondance, l'exception est considérée comme identifiée, et aucune autre recherche n'est lancée.
  Identifier une exception ne requiert pas de correspondance exacte entre l'exception et son identifiant. Un objet de classe dérivée pourra correspondre à un identifiant de la classe de base, comme montré dans cet exemple : "Index1125"

 
Sélectionnez
//: c10:Human.java
// Catching exception hierarchies.
class Annoyance  extends Exception {}
class Sneeze  extends Annoyance {}
public  class Human {
  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");
    }
  }
} ///:~



  L'exception Sneeze sera piégée par le premier catch (NdT cause) qui correspondra - qui sera le premier, bien sûr. Cependant, si vous retirez la première cause du piège, laissant seulement :

 
Sélectionnez
try {
      throw  new Sneeze();
    } catch(Annoyance a) {
      System.err.println("Caught Annoyance");
    }



  Le code sera toujours fonctionnel puisqu'il piège la classe de base de Sneeze. Placé d'une autre manière, catch(Annoyancee) identifiera une Annoyance ou toute autre classe qui en est dérivée. C'est utile puisque 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 piégera les exceptions de la classe de base.
  Si vous essayez de « masquer » les exceptions de la classe dérivée en plaçant le piège de classe de base en premier, comme ceci :

 
Sélectionnez
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, en voyant que la cause d'identification de Sneeze ne sera jamais atteinte.

XII-H-1. Recommandations pour les exceptions


Utilisez les exceptions pour :

  1. Fixer le problème et appeler la méthode causant cette exception une nouvelle fois ;
  2. Corriger les choses et continuer sans réessayer la méthode ;
  3. Calculer quelques résultats alternatifs à la place de ce que devrait produire la méthode ;
  4. Faire ce que vous désirez dans le contexte courant et relancer la même exception dans un contexte plus haut.
  5. Faire ce que vous désirez dans le contexte courant et lancer une exception différente dans un contexte plus haut ;
  6. Terminer le programme ;
  7. Simplifier. Si votre schéma d'exceptions rend les choses plus compliquées, alors il est douloureux et ennuyeux à utiliser ;
  8. Rendre votre bibliothèque et programme plus sûr. C'est un investissement à court terme pour le débogage, et un investissement à long terme pour la robustesse de l'application.

XII-I. Résumé


La recherche améliorée d'erreur est l'une des puissantes voies qui peuvent augmenter la robustesse de votre code. La recherche d'erreur est une préoccupation fondamentale pour chaque programme que vous écrivez, mais c'est surtout important en Java, où l'un des buts primaires est de créer des composants de programme pour d'autres utilisations. Pour créer un système robuste, chaque composant doit être robuste.

Les buts de l'identification d'exceptions en Java sont de simplifier la création de programmes larges, sûrs en utilisant le moins de code possible, et avec 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 à votre projet. Heureusement, Java impose tous les aspects des exceptions donc cela garantit qu'elles seront employées uniformément par à la fois les concepteurs de bibliothèques et les programmeurs clients.

XII-J. Exercices


Les solutions aux exercices sélectionnées peuvent être trouvées dans le document électronique The Thinking in Java Annotated Solution Guide disponible pour une modique somme à www.BruceEckel.com.

  1. Créez une classe avec un main() qui lance un objet de la classe Exception dans un bloc try. Donnez au constructeur d'exceptions un argument de type String. Piégez l'exception dans une cause catch et affichez l'argument du String. Ajoutez une cause finally et affichez un message pour prouver que vous vous situez là.
  2. Créez votre propre classe d'exceptions utilisant le mot-clef extends. Écrivez un constructeur pour cette classe qui prendra un argument de type String et stockez-le dedans l'objet par une référence au String. Écrivez une méthode qui imprime en sortie le String stocké. Créez une cause try-catch pour exercer votre nouvelle exception.
  3. Écrivez une classe avec une méthode qui lance une exception du type de celle créée dans l'exercice 2. Essayez de le compiler sans spécification d'exceptions afin de voir ce que dit le compilateur. Ajoutez la spécification d'exceptions adéquate. Essayez en sortie votre classe et son exception dans une cause try-catch.
  4. Définissez une référence objet et initialisez-la à null. Essayez d'appeler cette méthode grâce à cette référence. Maintenant, enveloppez le code dans une clause try-catch afin de capter l'exception.
  5. Créez une classe ayant deux méthodes, f() et g( ). Dans g(), lancez une exception d'un nouveau type que vous définirez. Dans f(), captez son exception et, dans la clause catch, lancez une exception différente (d'un second type que vous définirez). Testez votre code dans main( ).
  6. Créez trois nouveaux types d'exceptions. Écrivez une classe avec une méthode qui lance les trois. Dans main(), appelez la méthode, mais en utilisant seulement une seule clause catch qui piégera les trois types d'exceptions.
  7. Écrivez un code pour générer et capter une ArrayIndexOutOfBoundsException.
  8. Créez votre propre comportement de type résurrection en utilisant une boucle while qui se répète tant qu'une exception n'est pas lancée.
  9. 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 de 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 surtypez-le en A, puis appelez la méthode.
  10. Démontrez qu'un constructeur d'une classe dérivée ne peut pas capter les exceptions lancées par son constructeur de sa classe de base.
  11. Montrez que OnOffSwitch.java peut échouer en lançant une RuntimeException dans le bloc try.
  12. Montrez que WithFinally.java n'échoue pas en lançant une RuntimeException dans le bloc try.
  13. Modifiez l'exercice 6 en ajoutant une clause finally. Vérifiez que votre clause finally est exécutée, même si une NullPointerException est lancée.
  14. Créez un exemple où vous utiliserez 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 « Constructors ».
  15. Modifiez StormyInning.java en ajoutant une exception de type UmpireArgument, et les méthodes qui lancent cette exception. Testez la hiérarchie modifiée.
  16. Enlevez la première cause d'identification (catch) dans Human.java et vérifiez que le code compile et s'exécute toujours.
  17. Ajoutez un second niveau de perte d'exceptions à LostMessage.java afin que HoHumException soit lui-même remplacé par une troisième exception.
  18. Dans le titre VII, trouvez deux programmes appelés Assert.java et modifiez-les pour lancer leurs propres types d'exceptions au lieu d'imprimer vers System.err. Cette exception doit être une classe interne qui étend RuntimeException.
  19. Ajoutez un jeu d'exceptions appropriées à c08:GreenhouseControls.java.

précédentsommairesuivant
Le programmeur C peut regarder la valeur renvoyée par printf() en exemple à ceci.

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2013 Bruce Eckel. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.