Gestion avancée des exceptions
Classe Throwable
En Java, toutes les exceptions et erreurs sont des objets qui héritent de la classe Throwable.
Throwable est la classe de base pour tout ce qui peut être throw et catch. On ne l’utilise pas directement, il sert de base pour des types d’erreurs plus concrets.
Throwable
├── Exception
└── ErrorException - erreur normales
Exception est la classe de base pour toutes les opérations susceptibles de se produire dans un programme et qu’il faut et qu’on doit traiter.
Ce sont les erreurs opérationnelles: problèmes de fichiers, réseau, E/S, erreurs utilisateur.
La pluparts des try/catch travaille avec des sous classe Exception. Exemple:
IOException: erreur lors de la manipulation de fichier ou du réseauSQLException: erreur lors de l’accés à la base de donnéesFileNotFoundException: fichiers introuvable
Error - erreurs fatales de la JVM
Error est la classe de base des erreurs qui se produisent au niveau de la JVM. Ce sont généralement des défaillance critiques que le programme ne peut ni ne doit traiter. Si une Error survient, l’application ne pourra probablement pas continuer à s’exécuter. Exemple:
OutOfMemoryError: mémoire insuffisanteStackOverflowError: dépassement de pileNoClassDefFoundError: classe requise introuvable
Exceptions vérifiées et non vérifiées
En Java, les exceptions se répartissent en deux grandes catégories
Exceptions vérifiées (Checked)
Ce sont des exceptions que le compilateur oblige à traiter ou à relancer explicitement. Elles sont généralement liées à des ressources externes: fichiers, réseau, bases de données, saisie utilisateur.
Il faut les traiter avec un try/catch, soit ajouter un throws à la signature de la méthode
public void readFile(String path) throws IOException {
FileReader reader = new FileReader(path); // peut lancer IOException
// ...
}Exceptions non vérifiées (Unchecked)
Ce sont des exceptions qui ne nécessient pas de traitement obligatoire par le compilateur. Il s’agit généralement d’erreur de logique de programme: division par zéro, dépassement des limites d’un tableau, accès à null
On peut les intercepter, mais ce n’est pas obligatoire. Il vaut mieux les prévenir grâce à des vérifications.
int[] arr = {1, 2, 3};
System.out.println(arr[10]); // ArrayIndexOutOfBoundsExceptionPattern
Exception vérifiée: exemple avec des fichiers
Le compilateur oblige à traiter des IOException
import java.io.*;
public class FileDemo {
public static void main(String[] args) {
try {
FileReader reader = new FileReader("nofile.txt"); // FileNotFoundException (checked)
int data = reader.read();
System.out.println(data);
reader.close();
} catch (IOException e) {
System.out.println("Erreur lors de la manipulation du fichier : " + e.getMessage());
}
}
}Exception non vérifiée: exemple de division par zéro
Le compilateur ne demande aucun traitement, mais le programme “plante”
public class ExceptionDemo {
public static void main(String[] args) {
int a = 10;
int b = 0;
int c = a / b; // ArithmeticException (unchecked)
System.out.println("Résultat : " + c);
}
}Hiérarchie des exceptions
La hiérarchie des exceptions permet :
- Souplesse du traitement: on peut intercepter des erreurs spécifiques
FileNotFoundException, comme des groupes entiersIOExceptionouException - Réutilisation du code: possibilité de traiter de façon centralisées les erreurs d’un même type
- Propreté du code: la logique principale n’est pas encombrtée de vérification pour chaque détail
try {
// code risqué
} catch (FileNotFoundException e) {
System.out.println("Fichier introuvable !");
} catch (IOException e) {
System.out.println("Erreur d’entrée/sortie !");
} catch (Exception e) {
System.out.println("Quelque chose s’est mal passé : " + e.getMessage());
}Création d’exceptions personnalisées
Java fournis des exceptions comme NullPointerException, IllegalArgumentException, IOExceptio, mais il arrive que ces exceptions ne soit pas suffisament précise.
Les exceptions personnalisées sont une personnalisation pertinente de l’application. Elles permettent d’affiner la gestion des problémes, et via leur nom, indiquent immédiatement ce qui s’est passé.
Créer une nouvelle exception
On vient créer une classe qui hérite de l’une des classes d’exception standard :
Exception: pour les exception checked. Lorsque l’erreur est attendue et peut être géré (erreur de validation, fichier manquant)RuntimeException: pour les unchecked. Si l’erreur est lié à un bug dans la logique du programme (division par zéro, violation d’un invariant)
// checked
public class InvalidCredentialsException extends Exception {
public InvalidCredentialsException(String message) {
super(message); // On transmet le message à la classe parente
}
}
// utilisation
if (!login.equals("admin") || !password.equals("1234")) {
throw new InvalidCredentialsException("Identifiant ou mot de passe incorrect");
}
// ============================
// unchecked
public class NegativeBalanceException extends RuntimeException {
public NegativeBalanceException(String message) {
super(message);
}
}Constructeurs: render l’exception informative
Généralement, on implémente au moins un constructeur avec le paramètres String message. Mais on ajoute souvent d’autre :
public class ScoreLimitExceededException extends Exception {
public ScoreLimitExceededException() {
super();
}
public ScoreLimitExceededException(String message) {
super(message);
}
public ScoreLimitExceededException(String message, Throwable cause) {
super(message, cause);
}
public ScoreLimitExceededException(Throwable cause) {
super(cause);
}
}message: description textuelle de l’erreurcause: la cause (une autre exception), si on souhaite envelopper une erreur
Utilisation des exceptions personnalisées dans le code
On as un utilisateur, il a des points, et on ne peut pas en ajouter plus de 100
public class User {
private String name;
private int score;
public User(String name) {
this.name = name;
this.score = 0;
}
public void addScore(int points) throws ScoreLimitExceededException {
if (score + points > 100) {
throw new ScoreLimitExceededException("Limite de points dépassée ! Tentative d'ajout : " + points);
}
this.score += points;
}
}Classe d’exception
public class ScoreLimitExceededException extends Exception {
public ScoreLimitExceededException(String message) {
super(message);
}
}Traitement
try {
user.addScore(60);
user.addScore(50); // Une exception sera lancée ici ! Erreur: Limite de points dépassée ! Tentative d'ajout : 50
} catch (ScoreLimitExceededException e) {
System.out.println("Erreur: " + e.getMessage());
}Ajouter des champs personnalisées aux exception
Il est possible d’ajouter des champs supplémentaires qui aide à traiter l’erreur
public class ScoreLimitExceededException extends Exception {
private int currentScore;
private int attemptedAdd;
public ScoreLimitExceededException(String message, int currentScore, int attemptedAdd) {
super(message);
this.currentScore = currentScore;
this.attemptedAdd = attemptedAdd;
}
public int getCurrentScore() {
return currentScore;
}
public int getAttemptedAdd() {
return attemptedAdd;
}
}Utilisation
if (score + points > 100) {
throw new ScoreLimitExceededException(
"Limite de points dépassée !",
this.score,
points
);
}Bonne pratique
Nommage des exceptions
En Java, il est d’usage de nommer les exceptions utilisateur avec le suffixe Exception: InvalidUserInputException, InsufficientFundsException, …
Où et quand utiliser ses exceptions
- Lors de la validation des données utilisateurs
- En cas de violation de règles métier
- En cas d’erreur lors de l’appel de services externe
Chaînage d’exceptions
Le chaînage d’exception permet de ne pas perdre d’information sur la cause première de l’erreur, même si on envelopper une exception dans une autre.
Dans une application, il arrive souvent qu’une erreur se produise profondement dans la piste d’appels, par exemple, lors de l’utilisation d’une base de donnée, du systeme de fichiers ou du réseau.
Par exemple, une méthode qui interegose la base de données et peut lever une SQLException. Mais au niveau de la logique métier, on ne veut pas encombrer le code avec des détails techniques, et on préfère lever une exception, par exemple UserManagementException.
try {
// quelque chose avec la base de données
} catch (SQLException e) {
throw new UserManagementException("Erreur lors de la gestion des utilisateurs");
}Dans ce cas, l’information sur ce qui s’est précismenet produire dans la base de données et la pile d’appel est perdue. Dans le log, on obtient alors que UserManagementException sans connaitre la cause.
Envelopper l’exception d’origine (chaining)
Java permet d’envelopper une exception dans une autre en passant l’exception d’origine comme cause cause qu constructeur de la nouvelle exception. C’est le chaînage d’exception
La plupart des exceptions standard ou personnalisé possède un constructeur acceptant un second paramètre - Throwable cause
public UserManagementException(String message, Throwable cause) {
super(message, cause);
}Utilisation
try {
// quelque chose avec la base de données
} catch (SQLException e) {
throw new UserManagementException("Erreur lors de la gestion des utilisateurs", e);
}De cette manière, dans la stack trace, on voit à la fois l’exception et toute la chaîne jusqu’a la cause première.
Obtenir la cause d’une exception - getCause()
Tout objet de type Throwable possède une méthode getCause() qui renvoie l’expression d’origine ou null si elle n’existe pas.
try {
// ...
} catch (UserManagementException e) {
Throwable cause = e.getCause();
if (cause != null) {
System.out.println("Cause racine : " + cause);
}
e.printStackTrace();
}Cela permet:
- Pour le débogage: on voit seulement ce qui s’est mal passé au niveau supérieur, mais aussi où exactement l’erreur s’est produite dans la profondeur de la pile
- Pour la journalisation: on peut consigner toutes la chaîne d’erreur dans les journaux
- Pour transmettre l’information entre les couches de l’application: la couche métier peut envelopper une exception technique dans la sienne sans perdre de détails
Exemple: chaînage d’exception dans une application
On as une méthode qui charge un utilisateur à partir de la base de donnée
public User loadUser(String username) throws UserManagementException {
try {
// Code pouvant lever SQLException
// ...
} catch (SQLException e) {
throw new UserManagementException("Échec du chargement de l’utilisateur : " + username, e);
}
}
public class UserManagementException extends Exception {
public UserManagementException(String message, Throwable cause) {
super(message, cause);
}
}En cas d’erreur:
- Dans les journeaux, on voit à la fois l’exception
SQLExceptiond’origine avec tous les détails - Si nécessairem on peut accéder à la cause premiére via
getCause()
Pratique: implémentation du chaînage d’exceptions
Etape 1: création de l’exception
public class UserManagementException extends Exception {
public UserManagementException(String message) {
super(message);
}
public UserManagementException(String message, Throwable cause) {
super(message, cause);
}
}Etape 2: Utiliser le chaînage
try {
// quelque chose de risqué
} catch (SQLException e) {
throw new UserManagementException("Erreur lors des opérations sur la base de données", e);
}Etape 3: Traitement au niveau le plus haut Au niveau le plus haut du programme, on interce notre exception et affichons une message d’erreur ainsi que toute la chaîne des cause
public class Main {
public static void main(String[] args) {
try {
runUserManagement();
} catch (UserManagementException e) {
System.err.println("Une erreur s’est produite : " + e.getMessage());
// Nous affichons la chaîne des causes
Throwable cause = e.getCause();
while (cause != null) {
System.err.println("Cause : " + cause.getMessage());
cause = cause.getCause();
}
}
}
private static void runUserManagement() throws UserManagementException {
try {
// simulation d’une erreur de base de données
throw new SQLException("Aucune connexion à la base de données");
} catch (SQLException e) {
throw new UserManagementException("Erreur lors des opérations sur la base de données", e);
}
}
}Gestion avancée des exceptions et bonnes pratiques
catch multiple: gestion de différentes exceptions
Dans un programme, un bloc de code peut lever différentes types d’exceptions. Par exemple, lors de la lecture d’un fichier, on peut obtenir un FileNotFoundException, un IOException, et lors de l’analyse des données, un NumberFormatException.
Pour traiter correcte;ent chaque situation, Java permet d’enchaîne plusieurs bloc catch
try {
// opérations dangereuses
} catch (FileNotFoundException e) {
System.out.println("Fichier introuvable : " + e.getMessage());
} catch (IOException e) {
System.out.println("Erreur d’entrée/sortie : " + e.getMessage());
} catch (NumberFormatException e) {
System.out.println("Format de nombre incorrect : " + e.getMessage());
}Les blocs catch doivent aller du plus spécifique au plus général.
catch avec plusieurs types (multi-catch)
Parfois des exceptions différentes nécessitent le même traitement. Par exemple, on souhaite simplement afficher un message d’erreur ou la consigner dans les logs, qu’ils s’agissent d’une erreur de lecture de fichiers ou de conversion de nombre
try {
// opération dangereuse
} catch (IOException | NumberFormatException e) {
System.out.println("Erreur lors du traitement du fichier ou du nombre : " + e.getMessage());
}eaura le type de la superclasse commune la plus générale, généralementException- dans le bloc, il est impossible d’assigner une nouvelle valeur à la variable
e, elle est automatiquement considérée commefinal
try {
BufferedReader reader = new BufferedReader(new FileReader("numbers.txt"));
String line = reader.readLine();
int number = Integer.parseInt(line);
System.out.println("Nombre : " + number);
reader.close();
} catch (IOException | NumberFormatException e) {
System.out.println("Erreur : " + e.getMessage());
}Exceptions comme partie de l’API et try-with-ressources
Exception comme partie de l’API
Lorsque l’on écrit une méthode. on définit les paramètres et la valeur de retour mais également quelles exceptions elle peut lever. Cela fait partie du contrat entre la méthode et ceux qui vont l’utiliser. Si une méthode peut lever une exception, tous ceux qui l’appellent doivent le savoir, afin de gérer correctement l’erreur.
public void readFile(String filename) throws IOException {
// ... lecture du fichier
}Ici, on indique clairement que la méthode peut lever un IOException.
@throws - documentation des exceptions
L’annotation @throws permet de documenter les exceptions
/**
* Lit le contenu d’un fichier.
*
* @param filename nom du fichier
* @return le contenu du fichier sous forme de chaîne
* @throws IOException si une erreur de lecture du fichier s’est produite
*/
public String readFile(String filename) throws IOException {
// ...
}Construction try-withressources`
Dans de nombreuse tâches, il faut travailler avec des ressources qu’il est impératifs de fermer après usage: fichiers, connexion réseau, bases de données. Si on oublie de fermer une ressource, on obtient une fuite mémoire, un verouillage de fichier, ou d’autres désagréments.
Avant :
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("data.txt"));
String line = reader.readLine();
// ...
} catch (IOException e) {
// traitement de l’erreur
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
// traitement de l’erreur lors de la fermeture
}
}
}Avec la construction try-withressources, on vient fermer automatiquement toutes les ressources, même si une exception survient dans le bloc.
Syntaxe
try (ResourceType resource = new ResourceType(...)) {
// travail avec la ressource
} catch (ExceptionType e) {
// traitement de l’erreur
}Exemple
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("Erreur lors de la lecture du fichier : " + e.getMessage());
}
// reader.close() sera appelé automatiquement !- Entre parenthéses après
try, on déclare la ressources à fermer - A la sortie du bloc
try(même si une exception survient), la méthodeclose()est appelée pour chaque ressource - Cela ne fonctionne que pour les ressources qui implémentent l’interface
AutoCloseable
**Interface AutoCloseable
public interface AutoCloseable {
void close() throws Exception;
}Toutes les ressources standard de Java implémentent cette interface.
On peut venir déclarer plusieurs ressources
try (
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))
) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
}Les deux ressources seront fermées automatiquement, même si une exception survient
Pattern try-with-ressources
Lecture d’un fichier
public static void printFirstLine(String filename) {
try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
String line = reader.readLine();
System.out.println("Première ligne : " + line);
} catch (IOException e) {
System.out.println("Erreur : " + e.getMessage());
}
}Ecriture dans un fichier
public static void writeToFile(String filename, String text) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filename))) {
writer.write(text);
} catch (IOException e) {
System.out.println("Erreur lors de l’écriture : " + e.getMessage());
}
}Sa propre ressource
Si on écrit sa propre classe qui doit être fermée, on implémente AutoCloseable
public class MyResource implements AutoCloseable {
@Override
public void close() {
System.out.println("Ressource fermée !");
}
}On peut maitenant l’utiliser avec try-with-ressources
try (MyResource res = new MyResource()) {
// travail avec la ressource
}