Vous aimez ce que vous lisez sur ce blog ?
Envie d'aller plus loin avec véritable formation d'expertise en Java ?
Venez suivre ma formation Masterclasse Expertise Java !

"Même un développeur experimenté a besoin de continuer à apprendre. Et dans cette formation... j'ai appris beaucoup !" - A.G., Java Champion

Sessions intra-entreprises sur demande : contact[at]mokatech.net.
Inscrivez-vous vite !

Java Quiz #33

La classe ci-dessous part d'une bonne intention, mais où est le problème ?

  1. /**
  2.  * Garantit que les éventuelles exceptions levées par la méthode dangereuse
  3.  * seront loggées.
  4.  * @param <R> Le type de retour de la méthode
  5.  * @param <E> Le type d'exception levé
  6.  */
  7. public abstract class ExceptionLoggingExecutor<R, E extends Throwable> {
  8.  
  9. private final Logger logger = Logger.getLogger(ExceptionLoggingExecutor.class.getName());
  10.  
  11. public final R execute(Object... args) throws E {
  12. try {
  13. return dangerousOperation(args);
  14. } catch (E e) {
  15. logger.error(e);
  16. throw e;
  17. }
  18. }
  19.  
  20. /** La méthode contenant le code dangereux */
  21. protected abstract R dangerousOperation(Object... args) throws E;
  22. }

Réponse :
Ce code ne compile pas, car il est interdit d'utiliser des classes génériques dans un bloc catch.

La raison en est simple : l'effacement des types génériques qui survient lors de la compilation pourrait produire une incohérence dans l'ordre ds blocs catch d'un même bloc try.

L'exemple donné dans les commentaires par Colin Hébert le démontre bien :

  1. private static <T extends Exception, U extends Exception> void tryCatchThis() {
  2. try{
  3. }catch(T t){
  4. // traitement de T
  5. }catch(U u){
  6. // traitement de U
  7. }catch(Exception e){
  8. // traitement des autres exceptions
  9. }
  10. }

Rien ne permet au compilateur de vérifier que le bloc catch(T t) peut être placé avant le bloc catch(U u) pour toutes les valeurs de T et U.
Par exemple, l'utilisation des paramètres suivants serait illégale :

  1. MyClass.<IOException, UnknownHostException>tryCatchThis();

Par contre, vous remarquerez que rien n'interdit d'utiliser des types paramétrés dans la cause throws de la méthode - bien que j'en cherche encore un cas d'utilisation réaliste...


Commentaires

1. Le mardi 23 février 2010, 20:00 par rbrugier

Bonjour,

Un problème de conception ?

La classe qui va héritée de ExceptionLoggingExecutor va pouvoir surcharger la méthode execute et si la surcharge est mal réécrite, le log ne sera plus effectué.

Le fait de passer un tableau d'Object en argument de la méthode 'dangerousOperation' peut-être aussi, ça complique le contrôle de type sur les arguments.

2. Le mardi 23 février 2010, 20:09 par Hikage

Bon, premier commentaire non passé apparemment :)

J'aurais tendance à dire que comme les génériques ne sont pas "gardés" au runtime, mais utilise uniquement pour aider à l'écriture des sources, un catch(E e) deviendrait un catch(Object e) dans le bytecode.

Et a mon avis le compilateur ne doit pas le permettre.

3. Le mardi 23 février 2010, 20:41 par Olivier Croisier

Hummm, y'a de l'idée, mais ce n'est pas ça.
En effet, la clause E extends Throwable fait que le compilateur remplacera E par des Throwable et non des Object.

4. Le mardi 23 février 2010, 20:59 par Hikage

Autre piste, bien que je doute que cela soit celle que tu attends

Bien que protected, rien ne garanti que la méthode ne soit mise en "public" dans la classe fille, et donc d'être appellée directement, en outre passant la méthode execute.

5. Le mardi 23 février 2010, 21:00 par Colin HEBERT

Le problème du catch paramétré, c'est que si l'on s'amuse à en mettre plusieurs avec la même exception, ou alors sur le même arbre d'héritage, on peut alors ne plus respecter les règles concernant l'ordre des catch. Je suppose donc que c'est pour ça que la compilation va s'arrêter la.

Par contre pour le throws, je ne vois pas, à vue de nez, de problèmes avec cette syntaxe. Après tout, on a le droit de throws plusieurs fois la même exception.

6. Le mercredi 24 février 2010, 00:40 par Mathieu

Super ce quizz ! Plus dur que les autres :-)
Ca ne compile pas au catch() car le catch d'un type paramétrisé est une erreur car en effet ca change potentiellement la sémantique des catch. Ici il n'y en a qu'un seul mais on pourrait avoir
@@catch(E e) {...}
catch(NullPointerException e) {...}@@
Et apres la compil, ca devient:
@@catch(Exception e) {...}
catch(NullPointerException e) {...}@@
Pour régler correctement ce besoin, il faudrait connaitre le type de l'exception catchée à partir du type paramétrisé. Par example, faire un truc du genre:
@@public final R execute(Object... args) throws E {

   try {
       return dangerousOperation(args);
   } catch (Throwable e) {
       logger.error(e);
       // Error should never been catched !
       // Actually in this example the class signature should have been E extends Exception
       if(e instanceof Error)
           throw (Error) e;
       Class<E> exceptionType = getExceptionType(getClass());
       if (exceptionType.isInstance(e))
           throw exceptionType.cast(e);
       if(e instanceof RuntimeException)
           throw (RuntimeException) e;
       // should never go there, because we are in the case where
       // e is a must-catched exception
       RuntimeException re = new RuntimeException(e.getMessage(), e);
       re.setStacktrace(e.getStackTrace());
       throw re;
   }

}

private Class<E> getExceptionType(Class<? extends ExceptionLoggingExecutor> executor) {

   ParameterizedType executorType = (ParameterizedType) interfaceType.getGenericSuperclass();
   return (Class<E>) executorType.getActualTypeArguments()[2];

}@@

7. Le jeudi 25 février 2010, 21:09 par HollyDays

Hé ben cette fois-ci, j'aurai appris un truc !

Ceci dit, si le compilateur refuse effectivement de faire un "catch (E e)" ("Cannot catch type parameters"), je ne vois trop en quoi l'autoriser changerait fondamentalement la sémantique, puisque ce qui suit est légal :

} catch (Throwable e) {

   logger.error(e);
throw (E) e;

}
Et en gros, puisque les génériques disparaissent après la compilation, la compilation de la clause "catch" du quiz et de celle-ci devraient générer le même bytecode, au cast près. C'est curieux. C'est d'autant plus curieux que l'introduction des lambdas dans Java7 fera exploser allègrement cette contrainte, il me semble...

PS : Mathieu, en anglais, le participé passé de "to catch", c'est "caught", pas "catched" ! ;-)

PS 2 : Mathieu, par expérience, lorsqu'on développe un serveur que l'on veut robuste, on attrape même les "Error", même si c'est pour se contenter de générer du log et de la laisser remonter jusqu'à un point d'entrée standard (par exemple, le conteneur de servlets, qui, lui, attrape tout, y compris les Error, car un serveur, ça ne doit jamais s'arrêter sur erreur ; ça doit toujours pouvoir récupérer sur erreur).

Par ailleurs, il y a des "Error" qu'on peut être amené à attraper dans des cas bien particuliers, parce qu'on est capable de récupérer spécifiquement sur ces erreurs-là. Par exemple, une application client lourd qui doit charger le contenu d'un fichier (le "Open File" classique) devrait gérer les OutOfMemoryError qui sont lancées au cours de ce chargement : pour récupérer sur erreur, il suffit d'arrêter le chargement et la création de données correspondant au contenu du fichier, en mettant les références qu'il faut à null, et l'application s'en sort très bien.

Autre exemple : lorsqu'on charge des classes à la volée (par exemple parce qu'elles ont générées à la volée), leur chargement devrait toujours attraper VerifyError, car on n'est jamais à l'abri d'une erreur quelconque dans le contenu de la classe.

8. Le dimanche 28 février 2010, 13:58 par Colin HEBERT

HollyDays,
Cette syntaxe est interdite pour éviter les cas du genre :

   private static <T extends Exception, U extends Exception> void tryCatchThis(){
       try{
           //
       }catch(T e){
           //
       }catch(U e){
           //
       }catch(Exception e){
           //
       }
   }

Imaginons que l'on fasse cet appel :

   MyClass.<IOException, UnknownHostException>tryCatchThis();

Ou pire :

   MyClass.<IOException, IOException>tryCatchThis();

Ou encore :

   MyClass.<Exception, Exception>tryCatchThis();

Quel serait le résultat après compilation ?

Le seul moyen d'autoriser ceci serait de faire un système ou le try/catch ne pourrait contenir qu'un seul bloc catch, et si l'on doit en utiliser plusieurs, il faudrait alors les encapsuler. A ce moment là, la syntaxe devient plutôt complèxe et on perd totalement de vue le but du départ, qui était de simplifier la gestion des exceptions.

Concernant le P.S. 2, au risque de paraître trop attaché à la JavaDoc, celle-ci indique :
"An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions. The ThreadDeath error, though a "normal" condition, is also a subclass of Error because most applications should not try to catch it."

Donc effectivement dans _certains_ cas très précis (comme cité, la création d'un serveur d'application) il faudrait que l'application se remette d'une Error, mais ce n'est pas un état normal, ni un état "Exceptionnel", c'est que quelque chose de grave est arrivé et que l'application ne devrait pas continuer à tourner.

De la même manière, un développeur lambda ne doit _pas_ lancer d'error. Et ce serait un conseil pouvant porter préjudice, à tout débutant passant ici, que d'annoncer qu'une application devrait gérer, par exemple, les OutOfMemoryError.

9. Le dimanche 28 février 2010, 21:20 par Olivier Croisier

Effectivement, le problème se situe au niveau du catch() : pour les raisons que vous avez expliquées, il est illégal d'utiliser les génériques dans un bloc "catch()". Par contre, il est tout à fait valide de les utiliser dans la condition "throws" !

Quant à attraper Throwable, c'est évidemment fortement déconseillé pour un programme normal, mais assez courant sur certains middlewares, serveurs d'applications, ou frameworks servant de base à des systèmes à haute disponibilité, comme ceux sur lesquels Hollidays et moi avons travaillé.

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.