jeudi 1 novembre 2007

Du bon usage de /finally/

Dans le cadre du cours de réseaux informatiques, nous constatons malheureusement quelques légères difficultés avec de la matière de première année... un point particulièrement problématique est celui du traitement des exceptions en Java.

En particulier, la formation en première année (selon mes souvenirs) ne se concentre que sur l'aspect interception des exceptions, comme une nuisance mineure mais inévitable; la création d'exceptions propres au programme, et le report du traitement des erreurs, sont malheureusement mis de coté. Je ne crois pas avoir jamais fait d'exercice consistant a remonter une exception dans un cas concret.

L'intérêt des exceptions est évidemment de pouvoir déférer le traitement d'erreurs a une couche plus élevée du code, la ou il est possible de prendre une décision pertinente quant au traitement de l'erreur, et ce sans saturer l'interface de la méthode.

Exemple: voici une fonction sensée construire un objet 'blob' à partir d'un fichier:


Blob lire(String fichier) {
Blob b = ...
FileInputStream f = ...
...
f.close();
return b;
}

Mais si le fichier ne peut être lu ? d'ailleurs, le code précédent ne compile même pas; il y a plein d'exceptions IOException non récupérées. Que faire ? j'ai vu beaucoup de gens tomber dans le travers suivant:


Blob lire(String fichier) {
try {
Blob b = ...
...
return b;
} catch (Exception e) {
return null;
}
}


FAUX !!!
Non seulement le code appelant risque de se manger une NullPointerException à tout moment, mais en plus on perd complètement la traçe de l'erreur initiale. Et en plus, si un problème survient pendant la lecture (formatage, etc...) le fichier ne sera jamais fermé !

On réessaie:


Blob lire(String fichier) {
try {
FileInputStream f = ...
//fichier bien ouvert, il faut le fermer
Blob b;
try {
...
} catch (IOException e) {} //Erreur pendant lecture
f.close();
return b;
} catch (Exception e) {
return null;
}
}

ARCHI-FAUX !!!

La encore, on retourne un b qui risque d'être incorrect si le parsing n'a pas marché. Sans compter la laideur... notez qu'on ne peut retourner le b avant le f.close(), puisque le return termine la fonction, forcément...

On nous a enseigné que la syntaxe appropriée est:

try { ... } { { catch(Type e) { ... } } } finally { ... }

en première année, on se contente de dire que "le contenu du bloc finally est toujours exécuté après le contenu du bloc try, même si celui-ci a levé une exception.

Naif étudiant en première que j'étais, je me suis posé la question: Mais quelle est donc la différence entre:


try {
... //throws MyException
} catch (MyException e) {
...
} finally {
... // blah blah
}


et


try {
... //throws MyException
} catch (MyException e) {
...
}
// blah blah


? Dans les deux cas, si le contenu du bloc try lève une exception MyException, celle-ci est attrapée, et le code "blah blah" est exécuté. Alors pourquoi cette clause finally ?

Ce qui n'a pas assez été répété, c'est que le code dans le "try" peut très bien lever une autreexception que celle de la clause catch (qui est d'ailleurs facultative). D'une part, le code peut lever une exception de type RuntimeException (dont l'interception n'est pas obligatoire, car de telles exceptions sont considérées imprévisibles), d'autre part, la méthode contenant le bloc try/finally peut très bien déclarer elle-même lever des exceptions. Enfin, sachez également qu'il est possible de faire un "return" a l'intérieur d'un bloc try/finally, et que la clause finally sera exécutée tout de même !

Ce qui nous amène naturellement a la version légèrement plus correcte de l'exemple précédent:


Blob lire(String fichier) throws IOException {
FileInputStream f = ...
//fichier ouvert
try {
Blob b = ...
...
return b;
} finally {
f.close();
}
}


Notez que nous n'avons utilisé qu'un seul try/catch, que nous n'avons pas besoin de prédéclarer b dans un autre bloc (ce qui est laid), que f.close() sera toujours appelé si le fichier a été correctement ouvert, que le b retourné sera toujours correct, que le programme appelant aura a sa disposition toute l'information nécessaire pour réagir correctement à l'erreur, et que le compilateur informera le programmeur si le code de traitement d'erreur est oublié.

2 commentaires:

Jean a dit…

Merci pour le post...
Très intéressant. Je crois que j'aurais fait la version avec le try catch basic. Mais c'est vrai que ta version est un peu plus classe. Et je dois dire que j'ignorais totalement qu'on pouvais faire un try et finalement avec un throw. On m'a toujours présenté : soit mettre le try, catch, soit le throw, et j'ai jamais cherché plus loin.

Maxime Augier a dit…

Hourrah, un visiteur \o/ :D

Bon, je suis content que ça aie servi a quelqu'un comme ça !