juin
2010
Java se distingue des autres langages par sa gestion des exceptions qui introduit la notion de checked-exception, c’est à dire qu’il gère des exceptions vérifiées dès la phase de compilation, en forçant les développeurs à les gérer.
En clair, le compilateur analyse le code et génère des erreurs dans le cas où les exceptions ne sont ni traitées (via des try/catch
) ni déclarées dans la signature de la méthode (via throws
).
L’objectif original de tout cela était de pousser les développeurs à traiter correctement les exceptions. Malheureusement cela produit souvent l’effet inverse avec un traitement incorrect des exceptions, qui ne sont là uniquement pour éviter l’erreur du compilateur…
Il est en effet bien plus intéressant de se contenter de laisser remonter une exception plutôt que de se forcer à la traiter inutilement, car même si cela risque de planter le programme, on se retrouve avec une stacktrace propre et net permettant de retrouver rapidement l’origine exact du problème.
Toute la problématique vient du fait qu’on ne sait pas forcément quoi faire de l’exception, en particulier dans les cas où elle survient rarement, mais qu’on est quand même obliger de la traiter…
Prenons le cas de la gestion des entrées/sorties, qui génèrent en Java des IOException
s qu’il est obligatoire de traiter. Or dans bien des cas ces exceptions ne surviennent que dans des cas extrêmes, et l’on se retrouve alors devant un sacrée dilemme : que faire des exceptions ?
Par exemple, on souhaite utiliser une méthode permettant de charger une image depuis les ressources du jar, ce qui prendrait par exemple la forme suivante :
public class Tools {
public static Image load(String resourceName) throws IOException {
return ImageIO.read(Tools.class.getResource(resourceName));
}
}
La forme la plus logique serait de laisser remonter l’exception. Ainsi il est possible de gérer les éventuelles erreurs si besoin. Toutefois dans notre cas l’exception est assez embarrassante car elle nous oblige à la traiter à chaque fois. Notre code va alors se retrouver truffer de code de traitement d’erreur pour rien ou presque.
En effet les images étant packagées dans le jar, elles sont logiquement accessible et lisible à moins d’avoir un très gros problème sur la machine, ou d’avoir mal généré l’application.
Du coup on a tendance à utiliser quelque chose comme cela :
public static Image load(String resourceName) {
try {
return ImageIO.read(Tools.class.getResource(resourceName));
} catch (IOException e) {
e.printStackTrace(); // ou log
return null;
}
}
Malgré la présence d’une sortie console ou d’un log, ce code reste problématique. En effet le fait de retourner null laisse l’application dans un état inattendu (à moins que la nullité de toutes les images soient constamment vérifiée mais c’est aussi imbuvable que le traitement des exceptions).
Du coup, malgré la sortie console (ou le log), l’erreur peut passer inaperçu de prime abord (surtout si les données loggées sont assez conséquente, ou que l’on utilise une interface graphique). Par contre cela pourrait engendrer par ricochet d’autres problèmes bien plus tard dans le cycle de vie de l’application.
Du coup, à cause d’un mauvais déploiement, on peut se retrouver à rechercher l’origine d’un NPE assez étrange à première vue, tout simplement car l’erreur originelle est passé inaperçu.
Comme il n’y a pas vraiment de solution de contournement, le traitement de l’erreur ne fait qu’engendrer une autre erreur par la suite, moins évidente et donc plus difficile à corriger.
En fait la meilleure solution consiste à faire planter l’application. C’est le principe même du « fail-fast », c’est à dire de « planter rapidement« . En clair, plutôt que de continuer l’application comme si de rien n’était, au risque que cela engendre d’autres problèmes, on préfèrera arrêter l’application afin d’avoir le plus d’informations relative à l’erreur original, sans être « polluer » par d’éventuel autres problèmes que cela aurait engendré…
Dans ce cas là, la meilleure solution consiste à renvoyer des unchecked-exceptions, afin que le compilateur ne nous oblige plus à les traiter par la suite. En Java on utilise pour cela les RuntimeException
s et ses classes filles. On pourrait par exemple utiliser ceci :
public static Image load(String resourceName) {
try {
return ImageIO.read(Tools.class.getResource(resourceName));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
On se rapproche alors de ce qu’il se passe dans la plupart des autres langages où il n’y a aucune obligation de traiter les exceptions : l’erreur remontera alors la pile d’appel tant qu’elle n’est pas explicitement « catcher », éventuellement jusqu’à planter le programme ou le thread courant… mais on aura alors les bonnes informations sur la source du problème !
Il reste toutefois deux problématiques à utiliser cela :
- On change le type de l’exception qui est remontée, ce qui impose un traitement particulier de cette exception en cas de traitement global, comme par exemple via un
UncaughtExceptionHandler
. A moins de gérer une nouvelle sous-classes par type d’exception… - On dispose désormais d’une double stacktrace. Celle de l’exception originale qui nous est utile, et celle de la
RuntimeException
dont on pourrait se passer.Toutefois on peut éviter cela en utilisant une exception personnalisée dont on inhiberait la génération du stacktrace, ce qui se fait assez facilement en redéfinissant la méthode
fillInStackTrace()
:
public class UncheckedException extends RuntimeException {
public UncheckedException(Throwable source) {
super(source);
}
@Override
public Throwable fillInStackTrace() {
// On empêche la génération du stacktrace.
return this;
}
}
Mais la meilleure solution serait quand même de pouvoir ignorer une exception, même si c’est une « checked-exception ». Le langage n’autorise pas cela (et ce n’est apparemment pas prévu), mais il est possible de passer outre ces restrictions !
Les ckecked-exceptions n’existe pas !
La notion même de « check-exception » n’existe pas pour la machine virtuelle. A ce niveau là il n’y a strictement aucune différence de traitement entre les différents éléments « throwable« . Les classes Throwable
, Exception
, RuntimeException
et Error
et toutes leurs classes filles fonctionnent exactement de la même manière : elles peuvent être remontées n’importe où et éventuellement être interceptées par un try/catch.
En fait il s’agit uniquement d’une vérification du compilateur qui provoquera une erreur si l’on ne traite pas une « check-exception ».
Il existe pourtant un « hack » pour éviter cela, qui consiste tout simplement à tromper le compilateur en jouant un peu avec les Generics de Java 5.0 :
public class Exceptions {
public static <T extends Throwable> void throwMe(Exception e) throws T {
throw (T) e;
}
}
Cette méthode paramétrée par un élément <T extends Throwable>
remontera l’exception reçu en paramètre après l’avoir casté en T
, ce qui à première vue cela peut paraitre inutile (mais toute l’idée est dans ce petit bout de code).
En effet si on s’amuse à modifier le type paramétré de la méthode, on se retrouve dans un cas bien pratique :
Exceptions.<RuntimeException>throwMe(new IOException("BOUM"));
Cette syntaxe peu connu permet d’indiquer le type de paramétrage de la méthode lorsque celui-ci ne peut pas être déterminé à partir de ses paramètres.
Ici cela devrait générer un cast vers RuntimeException
, ce qui est illégal. Mais il faut prendre en compte le fonctionnement un peu particulier des Generics, qui « perd » le paramétrage à l’exécution (toute la vérification est effectué à la compilation).
Ainsi lors de l’appel de la ligne ci-dessus, le compilateur croit appeler une méthode avec une signature comme ceci :
public static void throwMe(Exception) throws RuntimeException
Alors qu’en réalité le code correspond plutôt à ceci puisqu’on utilise le type de base :
public static void throwMe(Exception) throws Throwable {
throw (Throwable) t;
}
En clair, le compilateur croit que le code ne peut remonter d’une RuntimeException
, alors qu’en réalité il pourra remonter n’importer quel élément throwable…
Bien sûr ce genre de pratique n’est pas totalement autorisé par le compilateur, qui signalera cet effet de bord par un warning que l’on pourra simplement ignoré via un @SuppressWarnings
.
On peut également utiliser une méthode supplémentaire qui nous permettra d’éviter de spécifier à chaque fois le paramétrage Generics. Ce qui peut donner au final :
public class Exceptions {
@SuppressWarnings("unchecked")
private static <T extends Throwable> T throwMe(Exception e) throws T {
throw (T) e;
}
public static RuntimeException unchecked(Exception e) {
return throwMe(e);
}
}
La méthode Exceptions.unchecked()
nous permet donc de remonter une « checked-exception » en passant outre les vérifications du compilateur, et donc de la traiter comme une « unchecked-exception ».
On peut donc modifier notre méthode load()
:
public class Tools {
public static Image load(String resourceName) {
try {
return ImageIO.read(Tools.class.getResource(resourceName));
} catch (IOException e) {
throw Exceptions.unchecked(e);
}
}
}
Et voilà ! Notre méthode Tools.load()
continue de renvoyer une IOException
, mais ne nous force plus à la traiter obligatoirement.
4 Commentaires + Ajouter un commentaire
Tutoriels
Discussions
- L'apparition du mot-clé const est-il prévu dans une version à venir du JDK?
- jre 1.5, tomcat 6.0 et multi processeurs
- Difference de performances Unix/Windows d'un programme?
- Définition exacte de @Override
- Recuperation du nom des parametres
- [ fuite ] memoire
- Possibilité d'accéder au type générique en runtime
- [REFLEXION] Connaitre toutes les classes qui implémentent une interface
- Classes, méthodes private
@kurzseb : En effet et c’est pour cela qu’il serait préférable que cela soit gérer au niveau du langage.
Toutefois on peut encore une fois tromper le compilateur avec les Generics et une méthode comme ceci :
Qui s’utiliserait comme ceci :
a++
C’est sympa, mais personnellement j’ai déjà eu le problème inverse : catcher une checked-exception remonté de façon silencieuse : le compilateur ne veut pas entendre parler du catch si une signature d’appel ne la déclare pas, il faut donc faire un try générique et ensuite rechercher la classe de l’exception : c’est pas propre du tout …
Oui j’ai bien précisé que c’était un « hack »
D’ailleurs il pose un autre problème, c’est qu’on ne peut plus catcher l’exception car le compilateur génèrera une erreur car elle n’est pas déclarée (« exception is never thrown »).
Toutefois il est toujours utile de remonter « silencieusement » une exception, quitte à l’englober dans une RuntimeException
a++
Je préfère largement l’idée de sous classer RuntimeException que celle d’utiliser un hack valable en Java5 qui introduis un warning et ne seras peut-être plus valable avec les prochaines versions ou avec un compilateur un peu plus malin.
Hormis c’est avis purement personnel, l’idée ne me semble pas mauvaise. Le seul inconvénient qui pourrais en résulter est que l’on se retrouvera avec uniquement des RuntimeException dans lesquelles il faudra au moins remonter le premier niveau de la trace pour avoir l’erreur originelle.