Accéder au contenu principal

Gestion des exceptions en Java: Comprendre, Traiter et Créer

Apprenez à gérer efficacement les exceptions d'exécution en Java, améliorant ainsi la fiabilité et la stabilité de vos applications et programmes.


1. Qu'est ce qu'une exception

Comme dans la plus part des languages de programmation, Java vous permet de gérer avec précision des erreurs imprévues (exceptions) qui pourraient survenir lors de l'exécution d'une instruction ou d'un traitement.

Une exception est une erreur imprévue qui survient lors de l'exécution d'une instruction de votre code (de votre programme).
Imaginons que vous souhaitiez faire la division de deux nombres dans votre programme: x = a / b.  Malheureusement si le diviseur b vaut zéro lors de l'exécution de votre programme, cette situation génèrera une erreur imprévue. On parlera alors d'une exception.

Il faut garder à l'esprit que toute instruction dans un programme peut générer une exception (à moins que vos instructions soient des plus basiques et simples).
Parfois lorsqu'une instruction survient dans votre programme, il n'est pas nécessaire que votre programme s'arrête aussitôt. Selon votre logique métier, votre programme pourrait continuer à fonctionner ou pas. Pour ce faire, il faut traiter la ou les exception potentielles de votre code. 

2. Comment traiter une exception

Java dispose du bloc try/catch/finally pour capturer et traiter les exceptions. Dans l'exemple ci-dessous décrivons ce qui ce passe dans chaque bloc du code.

private static final Logger log = Logger.getLogger(
ArithmeticExceptionExample.class.getName());

public static void main(String[] args){
int status = 0;
try {
// Le bloc try est exécuté en premier. Du moins, la machine essai d'exécuter
            // ce bloc
int a = 1;
int b = 7;
int x = a / b;
log.log(Level.INFO, "Result = " + x);
} catch (Exception e) {
// La machine arrête aussitôt l'exécution du bloc try lorsqu'une exception
            // survient et se met à exécuter le bloc catch
// Qui est en fin de compte le bloc réservé au comportement du programme
            // lorsqu'une exception survient:
// Traitement de l'exception
log.log(Level.SEVERE, "Unable to divide a by b: " + e.getMessage());
status = -1;
} finally {
// Le bloc finally (qui est optionnel) est tout le temps exécuté lorsqu'il
            // est présent. Son execution
// se fera soit à la suite du bloc try (lorsqu'aucune exception ne survient)
// Soit à la suite du catch en cas d'exception
System.exit(status);
}
}

Avec l'exemple ci dessus nous voyons apparaitre l'utilité des 3 blocs, et surtout comment capturer et traiter une exception en Java.

3. Exception vérifiée

Une exception vérifiée (checked) est une exception qui doit être traitée avec le bloc "try/catch" ou levée (ou déclarée) avec l'instruction throws, comme suit:

private static final Logger log = Logger.getLogger(CheckedExceptionExample.class.getName());
public static void main(String[] args) throws IOException {
Files.copy(Paths.get("myfile.txt"), System.out);
}

La vérification d'une telle exception est faite lors de la compilation du code java. Le compilateur a en charge la vérification du traitement ou de la levée d'une etelle exception.

Toutes les exceptions qui héritent de la classe "Exception" (à la seule différence de la classe RuntimeException) sont des exception vérifiées.

4. Exception non vérifiée

Une exception non-vérifiée (unchecked) est une exception dont la capture et le traitement ne sont pas vérifiées à la compilation du programme. Elles peuvent être vues comme des exception muettes qui ne se révèleront probablement que lors de l'exécution du programme.

private static final Logger log = Logger.getLogger(UnCheckedExceptionExample.class.getName());
public static void main(String[] args) {
String name = null;
log.info("The name length is: " + name.length());
}

Dans l'exemple ci dessus, l'exception NullPointerException (NPE) surviendra à l'exécution du programme. Mais à la compilation, le compilateur ne force en aucun cas le développeur à traiter, ou à déclarer cette exception, car l'exception NullPointerException est une RuntimeException.

Toutes les exceptions qui héritent de la classe "RuntimeException" sont des exception non-vérifiées.

5. Error vs Exception

Dans un programme java il existe une catégorie d'exceptions appelées erreur (error). Une erreur hérite de la classe de base de base de toutes exceptions: Throwable. Par contre il s'agit d'une exception non vérifiée qui ne devrait pas être capturée et traitée car elle indique une situation anormale du programme. A la différence des autres exceptions non vérifiée (RuntimeException).

Etant donnée qu'une telle erreur n devrait ni être capturée et ni être traitée, sa survenue engendre l'arrêt automatique de l'exécution du programme.
A titre d'exemple, nous pouvons citer: OutOfMemoryError, AssertionError, AbstractMethodError, ...

private static final Logger log = Logger.getLogger(AssertionErrorExample.class.getName());
public static void main(String[] args){
int a = 1;
int b = 0;
assert b != 0;
int x = a / b;
log.log(Level.INFO, "Result = " + x);
}

Dans l'exemple ci dessus, l'erreur AssertionError survient du fait de la violation de l'assertion: assert b!= 0.

Toutefois, il est possible de capturer une telle erreur en utilisant la classe de base: Throwable comme suit (meme si cela est fortement déconseillée du fait qu'une erreur traduit une situation anormale du programme et donc doit entrainer son arrêt immédiat).

private static final Logger log = Logger.getLogger(AssertionErrorExample.class.getName());
public static void main(String[] args){
int status = 0;
try {
int a = 1;
int b = 0;
assert b != 0;
int x = a / b;
log.log(Level.INFO, "Result = " + x);
} catch (Throwable e) {
log.log(Level.SEVERE, e.getMessage());
status = -1;
} finally {
System.exit(status);
}
}

6. Pourquoi créer ses propres exceptions

Lorsque vous développez une application, vous devez nécessairement traiter toutes les exceptions qui pourraient survenir.  Précédemment nous vous indiquions que toute instruction dans votre application (programme) pouvait potentiellement créer une exception.
Et lorsqu'une exception survient, vous devez inspecter les logs de votre application (ou la sortie des erreurs) afin de trouver et comprendre la cause de cette exception, puis résoudre le problème.

Pour faciliter cette résolution de problème, il faut nécessairement nommée et définir des exceptions précises qui permettront de déceler le lieu et la cause d'un problème survenu dans votre application.
Dans les bonnes pratiques Java, vous devez toujours créer (définir) vos propres exceptions en conservant la hiérarchie des exceptions.
Note: Vous pouvez créer des exceptions vérifiées et/ou des exceptions non-vérifiées

7. Comment créer ses propres exceptions

Dans notre exemple de division de deux nombres, nous allons structurer notre code de sorte à nous permettre de créer notre propre exception.
Dans une classe dédiée à ne faire que la division de deux nombres nous écrivons le code suivant:

public class DivideNumber {
// Le mot clé throws indique que cette méthode déclare l'exception
    // DivideNumberComputeException comme
// une exception qu'elle lèvera en cas de problème
public int compute(int a, int b) throws DivideNumberComputeException {
try {
return a / b;
} catch (Throwable e) {
String message = "Unable to divide a by b cause of: " + e.getMessage();
// Ici nous créons une instance de la classe d'exception en indiquant
            // un message contextuel clair et compréhensible
// Puis nous utilisons l'exception mère comme paramètre du constructeur
            // de cette instance afin de conserver
// la hiérarchie des exception utile lors de l'affichage de la stack trace
// le mot clé ici indique ici que nous levons cette exception
throw new DivideNumberComputeException(message, e);
}
}
}

Etant donné que l'instruction de division peut lever des exceptions, nous l'encapsulons dans un bloc try/catch. Dans le bloc catch nous levons une exception propre à nos besoins, que nous avons créé afin de bien spécifier et indiquer le potentiel problème qui pourrait survenir. 

public class DivideNumberComputeException extends Exception {
public DivideNumberComputeException() {
}

public DivideNumberComputeException(String message) {
super(message);
}

public DivideNumberComputeException(String message, Throwable cause) {
super(message, cause);
}

public DivideNumberComputeException(Throwable cause) {
super(cause);
}

public DivideNumberComputeException(String message, Throwable cause,
boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

Une classe d'exception doit toujours hériter des classes Throwable, Exception ou RuntimeException ou d'une autre exception.

Aussi, nous devons utiliser le mot clé throws au niveau de la signature de la méthode compute de la classe DivideNumber afin d'indiquer au compilateur java que notre méthode déclare une exception vérifiée.
Ainsi, à l'utilisation de la classe DivideNumber dans une méthode main par exemple nous devons nécessairement capturer et traiter l'exception DivideNumberComputeException ou la lever (déclarer) dans la signature de la méthode appelante (main) dans notre exemple comme suit:

public class DivideNumberExample {
private static final Logger log = Logger.getLogger(DivideNumberExample.class.getName());
public static void main(String[] args){
try {
int result = new DivideNumber().compute(10, 0);
log.log(Level.INFO, "Result is: " + result);
} catch (DivideNumberComputeException e) {
log.log(Level.SEVERE, e.getMessage(), e);
System.exit(-1);
}
}
}

Remarque: Dans la log, nous indiquons l'exception en plus du message de celle-ci afin que la hiérarchie des exceptions dans la stacktrace puisse nous permettre à comprendre la cause du problème, lorsqu'un problème survient.

SEVERE: Unable to divide a by b cause of: / by zero
com.javatutorialshub.exceptionhandling.divide.DivideNumberComputeException:
Unable to divide a by b cause of: / by zero
at com.javatutorialshub.exceptionhandling.divide.DivideNumber
.compute(DivideNumber.java:15)
at com.javatutorialshub.exceptionhandling.DivideNumberExample
.main(DivideNumberExample.java:13)
Caused by: java.lang.ArithmeticException: / by zero
at com.javatutorialshub.exceptionhandling.divide.DivideNumber
.compute(DivideNumber.java:8)
... 1 more

Le code source de ce tutoriel est présent sur github à cette adresse: java-exception-handling on github

Ne pas hésiter à commenter ou à poser une question ou à demander de l'aide autour de java et des technologies connexes. Nous nous ferons un plaisir de vous répondre.

Commentaires

Posts les plus consultés de ce blog

Comment valider les données utilisateur dans votre application Java : Guide pratique avec Jakarta Bean Validation

Lorsque vous développez une application qui interagit avec un utilisateur, en lui demandant de saisir des données, il est primordial de vérifier et valider ces données avant tout traitement. Et ceci pour plusieurs raisons: vous aurez la possibilité d'éviter des traitements inutiles si les données reçues ne sont pas conformes aux données attendues et aussi cela permet de protéger votre application en cas d'attaque orchestrée par des hackers. Dans le langage java, il existe une spécification qui vous permet de valider tout type de données transmis dans un bean: Java Bean Validation (ou plus récemment: Jakarta Bean Validation). Dans ce tutoriel, au travers d'une petite application de gestion d'une bibliothèque de quartier, nous allons voir ensemble comment utiliser Jakarta Bean Validation. 1. Pourquoi Jakarta Bean Validation L'objectif de Jakarta Bean Validation est de standardiser la validation des champs au travers de specification (JSR 380). Aussi cette standardisat...

Comprendre l'Inversion de Contrôle : Un Guide Pratique de Guice sur l'Injection des Dépendances dans le Développement de Logiciels

L'un des principes fondamentaux dans le développement de logiciels (ou d'applications) est la mise en oeuvre des principes SOLID. Le D ici indique: Dependency Inversion ou Dependency Injection ou encore Inversion Of Control (IoC). Autrement dit: Inversion de Contrôle ou Injection des Dépendances. L'injection des dépendances à pour principal objectif le découplage des modules entre eux. Cela implique donc de créer et développer les différents composants à base de classes abstraites ou d'interfaces, comme le permet Java. Ce faisant, l'application ou le logiciel devra utiliser une librairie ou un framework qui permette d'injecter les fameuses dépendances en question. Nous verrons dans la suite du tutoriel comment une librairie (ou framework léger) comme Guice (Google Guice) nous aidera dans cet objectif. Note: Spring Framework est l'un des framework les plus connus qui permettent de mettre en oeuvre l'Inversion de Contrôle.  1. Qu'est ce que l'Injec...