Accès rapide :
La vidéo
Qu'est qu'une exception ?
On traite l'exception.
Le bloc d'instructions try.
Le bloc d'instructions catch.
Le mot clé finally.
On relaye l'exception.
Le mot clé throws
Vous devez, normalement, dire ce que vous faites de vos exceptions
Les exceptions et la documentation de votre code
On déclenche une exception avec l'instruction throw
Cette vidéo vous présente les concepts de base du traitement d'exception en Java. Une exception java représente un état « exceptionnel » d'exécution de votre programme : une erreur. Les mots clés try, catch, throw et throws vous sont présentés.
Avant de rentrer dans les détails syntaxiques, je vais essayer de répondre à une première interrogation : qu'est qu'une exception ? Pour faire simple, une exception est une erreur produite durant l'exécution de votre programme. Sémantiquement parlant, le terme d'exception a été retenue car une erreur est censée être exceptionnelle : c'est n'est pas le mode de fonctionnement nominale de votre application.
La chose la plus importante à comprendre c'est qu'en cas de déclenchement d'exception (d'erreur) le programme passe dans un mode d'exécution
particulier : il faut qu'il trouve un gestionnaire d'erreur (un bloc d'instruction try
/ catch
). Analysons le programme suivant.
Via l'appel à la méthode Math.random
, ce programme a une chance sur trois de déclencher une exception (à cause de la division par 0,
qui rappelons le, n'est pas autorisée).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
package fr.koor.exceptions; public class TestException { public static void method3() { System.out.println( "BEGIN method3" ); int divisor = (int) (Math.random() * 3); int value = 1 / divisor; System.out.println( "Value == " + value ); System.out.println( "END method3" ); } public static void method2() { System.out.println( "BEGIN method2" ); method3(); System.out.println( "END method2" ); } public static void method1() { System.out.println( "BEGIN method1" ); method2(); System.out.println( "END method1" ); } public static void main(String[] args) { System.out.println( "BEGIN main" ); method1(); System.out.println( "END main" ); } } |
Comme vous le constatez, ce programme met en oeuvre une chaîne d'appels de méthodes. Bien entendu, le point d'entrée est le main
.
Puis il invoque method1
qui invoque à son tour method2
et qui finit par invoquer method3
. A chaque niveau
on trace l'entrée dans la méthode ainsi de la sortie. Comme indiqué précédemment, là où les choses se corsent, c'est en ligne 8, car on produit une
division par un entier qui peut valoir 0. Deux issues sont possibles pour ce programme.
Soit la valeur calculée aléatoirement vaut 1
ou 2
et dans ce cas, la division se passe correctement.
Du coup, l'exécution du programme se passe de manière traditionnelle : voici les résultats affichés sur la console.
$> java fr.koor.exceptions.TestException BEGIN main BEGIN method1 BEGIN method2 BEGIN method3 Value == 0 ( ou 1, si divisor == 1) END method3 END method2 END method1 END main$>
Soit la valeur calculée aléatoirement vaut 0
et dans ce cas une exception est déclenchée. Une fois l'exception produite (on dit aussi que
l'exception est levée) le système se met à la recherche d'un bloc de traitement d'erreur (un bloc try
/ catch
). Le souci, c'est
qu'il n'y a pas de tel bloc dans la méthode method3
: en conséquence l'exécution de la méthode est interrompue, et on se retrouve en ligne 15.
Toutes les variables locales définis par method3
(ainsi que les éventuels paramètres de la méthode) sont détruites
(on a dépilé au niveau de la stack). Mais nous sommes toujours en mode « exception levée » et donc le système cherche toujours à localiser un
bloc try
/ catch
. Il n'y en a pas non plus dans la méthode method2
, donc on abandonne l'exécution de cette méthode
et on dépile encore une fois. On se retrouve en ligne 21. Toujours pas de bloc try
/ catch
. On sort aussi de cette méthode
et on se retrouve en ligne 27. Toujours pas de try
/ catch
et du coup on sort de la méthode main
.
Et là me direz-vous ?
Et bien, le programme est terminé ! La JVM a récupéré l'exception et elle l'affiche sur la console, mais après elle rend simplement la main.
Le fait d'avoir suspendu l'exécution de toutes les méthodes garanti qu'aucun affichage de type END methodX
ne sera produit.
Voici un exemple d'exécution en cas de déclenchement d'exception.
$> java fr.koor.exceptions.TestException BEGIN method1 BEGIN method2 BEGIN method3 Exception in thread "main" java.lang.ArithmeticException: / by zero at fr.koor.exceptions.TestException.method3(TestException.java:8) at fr.koor.exceptions.TestException.method2(TestException.java:15) at fr.koor.exceptions.TestException.method1(TestException.java:21) at fr.koor.exceptions.TestException.main(TestException.java:27) $>
Donc c'est clair, il faut gérer les erreurs si vous ne voulez pas que votre programme s'arrête prématurément.
Pour intercepter et traiter une exception, on utilise l'instruction try / catch / finally
. Celle-ci peut être
constituée d'un nombre variable de blocs d'instructions en sachant que certains sont facultatifs (un peu comme le bloc else
dans une instruction
if / else
). Voici la syntaxe globale de l'instruction try / catch / finally
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
try { // Bloc d'instructions pouvant déclencher une erreur } catch ( Exception exception ) { // Bloc d'instructions pour traiter une éventuelle exception } finally { // Bloc d'instructions à exécuter dans tous les cas, // que le try se soit exécuté en succès ou en erreur. } |
Le bloc try
permet de définir un ensemble d'instructions à surveiller : la traduction du mot try
en français est « essayer ».
Ce bloc est obligatoire. Il ne peut pas y avoir de bloc catch
(ou finally
) sans bloc try
associé et les accolades y sont
obligatoires.
Si une exception est déclenchée durant l'exécution du bloc try
, il sera alors suspendu et l'exécution se poursuivra dans un bloc catch
associé ou dans le bloc finally
.
Il peut y avoir un ou plusieurs blocs catch
associé à un bloc try
.
Chaque bloc catch
est associé à un type d'erreur (une classe d'exception) à traiter : c'est ce qui est dit entre les parenthèses qui suivent
le mot clé catch
.
Si vous fournissez plusieurs blocs catch
, ils devront alors être ordonnés du bloc le plus spécifique ou bloc le plus général.
Si vous ne respectez pas l'ordre attendu, l'erreur de compilation suivante sera produite.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
try { BufferedReader reader = new BufferedReader( new InputStreamReader( System.in ) ); System.out.print( "Veuillez saisir un entier : " ); String strValue = reader.readLine(); int value = Integer.parseInt( strValue ); System.out.println( "Value == " + value ); } catch( Exception exception ) { System.out.println( "une exception quelconque" ); } catch( NumberFormatException exception ) { System.out.println( "La chaîne ne correspond pas à un entier" ); } |
Pour produire l'erreur de compilation :
$> javac Sample.java Sample.java:18: error: exception NumberFormatException has already been caught } catch( NumberFormatException exception ) { ^ 1 error $>
NumberFormatException
est déjà attrapée. Effectivement, comme la classe java.lang.NumberFormatException
dérive de la classe java.lang.Exception
, elle est déjà gérée par le premier bloc catch
associé au type Exception
.
La bonne syntaxe est donc la suivante.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
try { BufferedReader reader = new BufferedReader( new InputStreamReader( System.in ) ); System.out.print( "Veuillez saisir un entier : " ); String strValue = reader.readLine(); int value = Integer.parseInt( strValue ); System.out.println( "Value == " + value ); } catch( NumberFormatException exception ) { System.out.println( "La chaîne ne correspond pas à un entier" ); } catch( Exception exception ) { System.out.println( "une exception quelconque" ); } |
En cas de déclenchement d'exception, l'exécution du code est donc redirigée vers le bloc catch
le plus adapté.
Une fois le bloc catch
terminé, le bloc try
n'est pas rejoué. Si vous souhaitez que ce soit le cas, il vous appartient
d'utiliser une boucle pour retenter l'exécution du bloc try
. Voici un exemple de code tentant, au maximum trois fois, d'effectuer une
saisie de valeur numérique entière.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import java.io.BufferedReader; import java.io.InputStreamReader; public class Sample { public static void main( String [] args ) { int value = 0; int tryMaxCount = 3; while( tryMaxCount > 0 ) { try { BufferedReader reader = new BufferedReader( new InputStreamReader( System.in ) ); System.out.print( "Veuillez saisir un entier : " ); String strValue = reader.readLine(); value = Integer.parseInt( strValue ); break; // Pour sortir de la boucle while } catch( NumberFormatException exception ) { System.out.println( "La chaîne ne contient pas d'entier" ); } catch( Exception exception ) { System.out.println( "une exception quelconque" ); } tryMaxCount--; // On réduit le nombre de tentatives restantes. } if ( tryMaxCount > 0 ) { System.out.println( "Value == " + value ); } else { System.out.println( "Mais tu fais quoi ?" ); } } } |
Pour finir ces explications sur les blocs catch
, notez bien qu'il n'est pas obligatoire de fournir un bloc catch
,
mais dans ce cas un bloc finally
devra obligatoirement être spécifié.
Vous pouvez ajouter un dernier bloc à votre instruction try / catch
: le bloc finally
. Ce bloc est facultatif, mais si vous le
fournissez, il devra obligatoirement être positionné en dernier. Il permet de dire ce que vous souhaitez faire en fin d'instruction try
et ce
quelle que soit la manière dont il se termine (en succès ou en échec).
Voici un exemple de code qui permet de fermer deux fichiers après traitement, même si le traitement échoue. Dans notre cas, le traitement sera une copie de fichier.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class Copy { public static void main( String [] args ) { // Si vous ne fournissez pas deux paramètres au main, // on affiche l'usage de notre commande. if ( args.length == 0 ) { System.out.println( "Usage: java Copy sourceFile destFile" ); System.exit( 0 ); } // On calcule le nombre total d'octets du fichier source. long length = new File( args[0] ).length(); // On prépare trois variables qui correspondent au fichier source, // au fichier de destination ainsi qu'un buffer d'octets. FileInputStream inputStream = null; FileOutputStream outputStream = null; byte [] buffer = new byte[ 1024 * 1024 ]; try { // On ouvre les deux fichiers (en lecture et en écriture) inputStream = new FileInputStream( args[0] ); outputStream = new FileOutputStream( args[1] ); // On recopie les octets du fichier, tant qu'il y en a. while ( length > 0 ) { int readedBytes = inputStream.read( buffer ); outputStream.write( buffer, 0, readedBytes ); length -= readedBytes; } System.out.println( "Copie du fichier terminée" ); } catch( IOException exception ) { // Il y a donc eu une erreur durant la copie des fichiers System.err.println( "Impossible de réaliser la copie du fichier" ); } catch( SecurityException exception ) { // On a un problème de droits sur les fichiers (java.lang.SecurityException) System.err.println( "Vous n'avez pas les droits pour réaliser la copie du fichier" ); } finally { // On ferme les fichiers. if ( inputStream != null ) { try { inputStream.close(); } catch( Exception e ) { /* Tant pis */ } } if ( outputStream != null ) { try { outputStream.close(); } catch( Exception e ) { /* Tant pis */ } } } } } |
Comme nous l'avons dit, nous pouvons aussi avoir une instruction try / finally
sans bloc catch
. En voici un petit exemple.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class Sample { public static void main( String [] args ) { try { int randomValue = (int) (Math.random() * 3); // Attention : cà peut produire une erreur en cas de division par 0 int res = 1 / randomValue; System.out.println( "Résultat == " + res ); } finally { System.out.println( "On a terminé !" ); } } } |
Du coup, soit une exception est produite et vous observerez le résultat suivant.
Exception in thread "main" java.lang.ArithmeticException: / by zero at Sample.main(Sample.java:8) On a terminé !
Soit tout se passe bien et vous aurez le résultat suivant.
Résultat == 0 On a terminé !
Vous n'êtes pas obligé de traiter votre erreur au niveau ou vous êtes. Dans ce cas, il faut indiquer sur la signature de la méthode que vous relayez
le traitement de l'exception au niveau de la méthode « appelante ». Cela se fait en utilisant le mot clé throws
à la suite de la
liste de paramètres de la méthode. Voici un exemple de syntaxe.
1 2 3 |
public void aMethod() throws IOException { // . . . } |
throw
(sans « s » final) permet
de déclencher une erreur, alors que le mot clé throws
(avec le « s » final) permet de relayer le traitement de l'exception à la méthode
appelante.
Passons à un exemple un peu plus concret : on reprend la copie de fichier proposée précédemment, mais notez que cette fois-ci, en cas d'erreur,
on botte en touche : la méthode main
relaye l'exception au niveau supérieur. Sauf que dans notre cas, le niveau supérieur, c'est la JVM : celle-ci
interceptera les éventuelles exceptions et se contentera de les afficher sur la console.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class Copy { public static void main( String [] args ) throws IOException { // Si vous ne fournissez pas deux paramètres au main, // on affiche l'usage de notre commande. if ( args.length == 0 ) { System.out.println( "Usage: java Copy sourceFile destFile" ); System.exit( 0 ); } // On calcule le nombre total d'octets du fichier source. long length = new File( args[0] ).length(); // On prépare trois variables qui correspondent au fichier source, // au fichier de destination ainsi qu'un buffer d'octets. FileInputStream inputStream = null; FileOutputStream outputStream = null; byte [] buffer = new byte[ 1024 * 1024 ]; try { inputStream = new FileInputStream( args[0] ); outputStream = new FileOutputStream( args[1] ); while ( length > 0 ) { int readedBytes = inputStream.read( buffer ); outputStream.write( buffer, 0, readedBytes ); length -= readedBytes; } System.out.println( "Copie du fichier terminée" ); } finally { if ( inputStream != null ) { try { inputStream.close(); } catch( Exception e ) { /* Tant pis */ } } if ( outputStream != null ) { try { outputStream.close(); } catch( Exception e ) { /* Tant pis */ } } } } } |
Si vous lancez votre programme en spécifiant un fichier source non existant, vous pourrez obtenir ce type de message d'erreur sur la console.
Exception in thread "main" java.io.FileNotFoundException: aFile.txt (Aucun fichier ou dossier de ce type) at java.io.FileInputStream.open0(Native Method) at java.io.FileInputStream.open(FileInputStream.java:195) at java.io.FileInputStream.<init>(FileInputStream.java:138) at java.io.FileInputStream.<init>(FileInputStream.java:93) at Copy.main(Copy.java:27)
A savoir, sur une déclaration de méthodes, vous pouvez spécifier plusieurs types d'exceptions pouvant être relayée à la méthode appelante.
1 2 3 |
public void aMethod() throws IOException, SQLException { // . . . } |
Il y a, en Java, une règle très importante sur la gestion des exceptions : sauf dans un cas particulier (nous allons y revenir plus loin), vous devez
obligatoirement dire ce que vous faites en cas de déclenchement d'exception. Soit vous traitez l'exception, via un bloc try / catch
, soit vous
relayez le traitement de l'exception à la méthode appelante via le mot clé throws
. Si vous dite rien quant au traitement d'une exception,
une erreur de compilation sera inexorablement produite. Voici un exemple de code produisant une telle erreur de compilation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; public class Copy { public static void main( String [] args ) { // Si vous ne fournissez pas deux paramètres au main, // on affiche l'usage de notre commande. if ( args.length == 0 ) { System.out.println( "Usage: java Copy sourceFile destFile" ); System.exit( 0 ); } // On calcule le nombre total d'octets du fichier source. long length = new File( args[0] ).length(); // On prépare trois variables qui correspondent au fichier source, // au fichier de destination ainsi qu'un buffer d'octets. FileInputStream inputStream = null; FileOutputStream outputStream = null; byte [] buffer = new byte[ 1024 * 1024 ]; try { inputStream = new FileInputStream( args[0] ); outputStream = new FileOutputStream( args[1] ); while ( length > 0 ) { int readedBytes = inputStream.read( buffer ); outputStream.write( buffer, 0, readedBytes ); length -= readedBytes; } System.out.println( "Copie du fichier terminée" ); } finally { if ( inputStream != null ) { try { inputStream.close(); } catch( Exception e ) { /* Tant pis */ } } if ( outputStream != null ) { try { outputStream.close(); } catch( Exception e ) { /* Tant pis */ } } } } } |
Et voici les erreurs de compilation produites par ce programme (il y a effectivement plusieurs lignes de codes pouvant déclencher une erreur).
$> javac Copy.java Copy.java:26: error: unreported exception FileNotFoundException; must be caught or declared to be thrown inputStream = new FileInputStream( args[0] ); ^ Copy.java:27: error: unreported exception FileNotFoundException; must be caught or declared to be thrown outputStream = new FileOutputStream( args[1] ); ^ Copy.java:30: error: unreported exception IOException; must be caught or declared to be thrown int readedBytes = inputStream.read( buffer ); ^ Copy.java:31: error: unreported exception IOException; must be caught or declared to be thrown outputStream.write( buffer, 0, readedBytes ); ^ 4 errors $>
Notez aussi que, si vous utiliser un IDE tel qu'Eclipse, ces erreurs vous sont directement signifiées dans l'éditeur de code Java. Voici une capture d'écran montrant ces erreurs.
Si vous êtes déjà développeur C++ ou C#, vous vous posez peut-être une question. Dans ces langages, il n'est pas obligatoire de dire ce qu'on fait en cas d'erreur. Alors pourquoi, en Java, il est obligatoire de le faire ?
Il y a plusieurs manières que justifier ce choix. Pour moi, la réponse la plus importante est que si vous n'étiez pas conscient qu'un code pouvait planter,
le fait d'avoir produit l'erreur de compilation fait que maintenant vous l'êtes. Et du coup, vous produisez des codes forcément plus robustes (sauf si
vous bottez en touche avec l'instruction throws
, bien entendu).
La deuxième justification importante, de mon point de vue, est que, comme l'utilisation du throws
fait partie de la signature de la méthode,
quand vous produirez automatiquement ou utilisez des documentations Javadoc, l'information comme quoi telle méthode
peut potentiellement produire une erreur vous sera systématiquement proposée (et y compris de manière intégrée à Eclipse). Voici un exemple de documentation
Javadoc indiquant qu'une exception peut être déclenchée (méthode java.io.FileInputStream.read( byte [] buffer)
).
Nous reviendrons sur la manière de produire une documentation au format « Javadoc » dans un prochain chapitre.
Ceux qui ont lu les chapitres précédents de ce cours/tutoriel de programmation Java auront certainement remarqué que nous avons déjà un peu parlé de comment
déclencher une exception : effectivement, dans le chapitre de programmation orientée objet consacré à l'encapsulation
nous avions vérifié que la valeur passé en dénominateur d'un nombre rationnel (une fraction) soit bien différente de 0. Si ce n'était pas le cas, nous produisions
une erreur via le mot clé throw
. Pour rappel, voici le code que nous avions proposé pour la méthode setDenominator
.
1 2 3 4 5 6 |
public void setDenominator( int denominator ) { if ( denominator == 0 ) { throw new RuntimeException( "denominator cannot be zero" ); } this.denominator = denominator; } |
throw
(sans « s » final) permet
de déclencher une erreur, alors que le mot clé throws
(avec le « s » final) permet de relayer le traitement de l'exception à la méthode
appelante.
L'instruction throw
permet de déclencher l'instance d'exception passée en argument. Dans notre cas, il s'agit d'une nouvelle instance (utilisation
de l'opérateur new), mais vous auriez pu préalablement produire l'instance, puis la déclencher ultérieurement. La syntaxe suivante est tout aussi valide.
1 2 3 4 5 6 7 |
public void setDenominator( int denominator ) { if ( denominator == 0 ) { RuntimeException exc = new RuntimeException( "denominator cannot be zero" ); throw exc; } this.denominator = denominator; } |
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :