Le mécanise de gestion des exceptions en Java permet de traiter des situations exceptionnelles: il permet de s’affranchir de structures de contrôles qui alourdiraient le code.
Ce mécanisme doit être utilisé en suivant certaines règles qui permettent d’adapter le comportement du programme au type de situation rencontrée.
Dans une voiture, le bon fonctionnement du moteur dépend d’autres organes. Par exemple, le moteur ayant besoin de carburant, celui-ci est acheminé jusqu’au système d’injection par la pompe à carburant. Cette même pompe puise dans le réservoir de carburant.
Réservoir ==> Pompe à carburant ==> Injection ==> Moteur
Le moteur ne peut plus fonctionner lorsque le réservoir est vide. Cette situation étant exceptionnelle, l’absence de carburant entraînerait, dans un programme Java, la levée d’une exception.
\nearrow EXCEPTION !
Réservoir ==> Pompe à carburant ==> Injection ==> Moteur
Un programme bien écrit va permettre de transmettre l’exception au moteur et donc de l’arrêter proprement.
Arrêter proprement un programme revient à éviter qu’il ne se retrouve dans un état instable.
Une fois dans un état instable, un programme ne sera plus capable de fonctionner correctement. Il aura un comportement imprévisible, voire dangereux.
Si on représentait le fonctionnement du moteur avec un programme Java, on pourrait écrire ceci:
class Moteur {
private SystemeInjection systemeInjection ;
...
public void tourne(){
systemeInjection.injecteCarburant(quantiteNecessaire, this.cylindre1 , this.cylindre2, this.cylindre3, this.cylindre4);
...
}
}
class SystemeInjection {
private PompeACarburant pompeACarburant ;
...
public void injecteCarburant(int quantite, Cylindre c1, Cylindre c2, Cylindre c3, Cylindre c4){
pompeACarburant.getCarburant(quantite);
...
}
}
class PompeACarburant{
private Reservoir reservoir;
...
public void getCarburant(int quantite){
int quantitePrelevee = reservoir.fournirCarburant(quantiteCarburant); // Que se passe-t-il si la quantité de carburant est supérieur à ce qui reste dans le réservoir ?
}
}
class Reservoir{
private int quantiteCarburant; //la quantité de carburant dans le réservoir.
public int fournirCarburant(int quantite){
// On ne peut pas avoir une quantite de carburant négative dans le réservoir, comment signaler cela à l'appelant (la pompe à carburant)
}
}
Lorsque le réservoir est vide, on doit se poser la question de savoir qui est responsable de traiter cette situation exceptionnelle:
-
le réservoir ?
-
la pompe à carburant ?
-
la pompe à injection ?
-
le moteur ?
Autrement dit, quel comportement adopter et à quel moment dans le déroulement du programme ?
En anglais, the stack, the caller and the callee .
Pour savoir si une exception doit être levée (throw
) ou attrappée
(catch
), il faut se poser la question suivante: a-t-on les moyens de
traiter correctement la situation exceptionnelle à ce niveau du
programme ?
Si ce n’est pas le cas, l’exception sera passée à l’appelant, via une
clause throw
.
Si c’est le cas, on va pouvoir la traiter via une clause try
/catch
(éventuellement finally
)
Dans notre exemple, il est intéressant de se poser la question: qu’est-ce qui fait que le moteur s’arrête ?
C’est bien l’absence de carburant dans les cylindres qui provoque l’arrêt du moteur. Cependant, on va lever une exception lorsque la pompe à carburant tentera de prélever trop de carburant depuis le réservoir.
Pour cela, la méthode fournirCarburant lèvera une exception si la quantité demandée est trop importante:
class Reservoir
private int quantiteCarburant; //la quantité de carburant dans le réservoir. public int fournirCarburant(int quantite) throws PasAssezDeCarburantException
Toujours dans l’analogie avec le moteur, occupons-nous d’une partie vitale pour celui-ci:
le circuit d’huile
Autant le moteur peut se passer de carburant sans trop de dommage, autant le faire tourner sans huile conduit à sa destruction certaine.
Comment géreriez-vous cela en utilisant les exceptions ?
public class ErreurMemoire {
public static void main(String[] args) {
double[] tableauTropGrandPourLaMemoire = new double[1000000000];
}
}
On pourrait aisément traiter cette erreur de cette manière:
public class ErreurMemoirePasse {
public static void main(String[] args) {
try {
double[] tableauTropGrandPourLaMemoire = new double[1000000000];
} catch (Error e) {
System.out.println("On continue malgré l'erreur mémoire");
}
System.out.println("On sort (que peut-on faire d'autre sans mémoire ?)");
}
}
Quels sont les avantages et inconvénients de ce code ?
La classe Exception est la super-classe de toutes les exceptions levées
lorsqu’une erreur survient dans une application Java. Elle est propagée
jusqu’à être prise en charge, soit par un bloc catch
, soit en étant
affichée par la sortie d’erreur (System.err
) ou la sortie standard.
public class LeveeExceptionMain {
public static void main(String[] args) {
int[] tableau = new int[10];
int valeur = tableau[10];
System.out.println("Fin de l'exécution");
}
}
La manière classique de traiter une exception est de l’attraper en
définissant un bloc try-catch
.
public class LeveeExceptionTraitee {
public static void main(String[] args) {
int[] tableau = new int[10];
try {
int valeur = tableau[10];
System.out.println("valeur="+valeur);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Traitement de l'exception");
}
System.out.println("Fin de l'exécution");
}
}
Une erreur de compilation courante:
public class LeveeExceptionTraitee {
public static void main(String[] args) {
int[] tableau = new int[10];
try {
int valeur = tableau[10];
System.out.println("valeur="+valeur);
} catch (Exception e) {
System.out.println("Traitement de l'exception");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("ArrayIndexOutOfBoundsException attrapée");
}
System.out.println("Fin de l'exécution");
}
}
Réécrire le code correct pour ce cas.
Un bloc finally
situé après un bloc try
sera toujours exécuté, même
si des exceptions sont levées pendant l’exécution du bloc try
. Par
exemple:
public class ExempleFinally {
public static void main(String[] args) {
int[] tableau = new int[10];
try {
int valeur = tableau[10];
System.out.println("valeur="+valeur);
} finally {
System.out.println("On exécute toujours cette portion de code");
}
System.out.println("Fin de l'exécution");
}
}
On peut générer ses propres exceptions:
package fr.viacesi.ap2017.humain;
public class TailleInvalideException extends Exception {
public TailleInvalideException() {
super();
}
public TailleInvalideException(String message, Throwable cause) {
super(message, cause);
}
public TailleInvalideException(String message) {
super(message);
}
public TailleInvalideException(Throwable cause) {
super(cause);
}
}
Que l’on va utiliser dans notre code:
package fr.viacesi.ap2017.humain;
public class Humain {
private int taille;
public int getTaille() {
return taille;
}
public void setTaille(int taille) throws TailleInvalideException {
if(taille < 0 || taille > 300) {
throw new TailleInvalideException("taille "+taille+" n'est pas valide.");
}
this.taille = taille;
}
private String nom, prenom;
public Humain(String nom, String prenom){
this.nom = nom;
this.prenom = prenom;
}
@Override
public String toString() {
return super.toString()+ "/"+nom+" "+prenom;
}
}
Ecrivez le programme permettant de gérer cette exception.
On définit une classe NePeutPasAvoirEnfantException qui hérite de RuntimeException. On l’utilise ici:
public void ajouteEnfant(Humain enfant){
if (age<15) {
throw new NePeutPasAvoirEnfantException("Je suis trop jeune !");
}
}
Qu’observez-vous ?
Les nouvelles classes d’exception ne sont à créer que si réellement nécessaire. Toujours se poser la question de savoir si une exception standard ou existante ne pourrait pas être utilisée.
public class NeJamaisFaireCeci {
public static void main(String[] args) {
try{
int valeur = tableau[10];
System.out.println("valeur="+valeur);
}catch(Exception ex){}
}
}
Sur un code finalisé, tout bloc catch
doit contenir un traitement
adapté de l’exception. ex.printStackTrace()
n’est pas suffisant.
Sur un programme en cours de développement, on tolère que le bloc
catch
ne contienne que :
-
un appel à
ex.printStackTrace()
-
un message indiquant que ce code doit être complété (ex:
/* TODO: traiter l’exception */
).
Exécuter le code suivant:
public class ReturnDansFinally {
public String methodeFinally(){
try{
return "Je devrais sortir";
}finally{
return "Pourtant c'est moi qui sort !";
}
}
public String methodeException(){
try{
return "Je sors";
}catch(Exception e){
return "Je ne sortirai pas";
}
}
public static void main(String[] args) {
ReturnDansFinally rdf = new ReturnDansFinally();
System.out.println("methodeFinally dit : "+rdf.methodeFinally());
System.out.println("methodeException dit : "+rdf.methodeException());
}
}
Qu’observez-vous en exécutant ce code ?
Qu’en déduisez-vous par rapport à la meilleure manière de coder avec
finally
?
Le type de sortie d’une boucle tel qu’utilisé ici est à proscrire:
for (int i = 0; i < args.length; i++) {
if(args[i] = null){
throw new RuntimeException("Fin de boucle");
}
int valeur = args[i] + valeur;
}
Que pouvez-vous utiliser à la place ?
Un exemple de bonne gestion des erreurs lors des accès à un fichier.
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.FileReader;
/**
* Classe de lecture de fichier gérant les erreurs d'entrée/sortie
*/
public class LectureFichierAvecException {
public void lecture(){
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("unfichier.txt"));
String ligne;
while((ligne=br.readLine())!=null){
System.out.println(ligne);
}
} catch (FileNotFoundException ex) {
System.out.println("Se plaindre à l'utilisateur");
System.out.println("Le fichier n'existe pas !!!");
} catch (IOException ex) {
System.out.println("Informer l'utilisateur");
System.out.println("Il y a eu une erreur");
} finally {
if (br != null) {
try {
br.close();
} catch (IOException ex) {
System.out.println("Cela ne devrait pas arriver, mais on le signale quand même.");
}
System.exit(1); //Un code d'erreur signale l'erreur
}
}
}
public static void main(String[] args) {
new LectureFichierAvecException().lecture();
}
}
Exécutez le code suivant:
public class UnTestAvecUneExceptionGenereeParUneAutre {
public static void main(String[] args) throws Exception {
try{
throw new Exception("Je vais me cacher");
}catch(Exception ex){
throw new Exception("Je me montre");
}
}
}
Q’observez-vous ?
Utilisez plutôt, dans le bloc catch
:
throw new Exception(Je me montre, ex);
Soit les classes d’exception telles que:
Ne pas écrire simplement:
public void grandir() throws PersonneException{
//Un traitement ici
}
Mais déclarer toutes les exceptions :
public void grandir() throws PersonneException,
GarconException,
HommeException {
//Un traitement ici
}
Considérons le code suivant:
Class classe = null;
Object objet = null;
try {
type = Class.forName("unknown.package.Classe");
// throws ClassNotFoundException
} catch (ClassNotFoundException e) {
e.printStackTrace();
//Traiter ClassNotFoundException
}
try {
object = type.newInstance();
// throws InstantiationException, IllegalAccessException
} catch (InstantiationException e) {
e.printStackTrace();
//Traiter InstantiationException
} catch (IllegalAccessException e) {
e.printStackTrace();
//Traiter IllegalAccessException
}
String string = object.toString();
Que se passe-t-il si l’on exécute ce code ?