Le langage de programmation Java, comme beaucoup d'autres, dispose d'outils intégrés pour gérer les erreurs, c’est-à-dire les situations exceptionnelles (exceptions) où une défaillance du programme est traitée par un code spécifique, séparé de l’algorithme principal.
Grâce aux exceptions, un développeur peut anticiper les points faibles du code et éviter les erreurs fatales lors de l’exécution.
Ainsi, la gestion des exceptions en Java est une bonne pratique qui améliore la fiabilité globale du code.
L’objectif de cet article est d’explorer les principes de capture et de gestion des exceptions, ainsi que d’examiner les structures syntaxiques correspondantes du langage prévues à cet effet.
Tous les exemples de ce guide ont été exécutés sur Ubuntu 22.04, installé sur un serveur cloud de Hostman.
Les exemples présentés dans ce guide ont été exécutés à l’aide d’OpenJDK. Son installation est simple.
Tout d’abord, mettez à jour la liste des dépôts disponibles :
sudo apt update
Ensuite, affichez la liste des versions d’OpenJDK disponibles au téléchargement :
sudo apt search openjdk | grep -E 'openjdk-.*-jdk/'
Vous verrez une courte liste dans le terminal :
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
openjdk-11-jdk/jammy-updates,jammy-security 11.0.25+9-1ubuntu1~22.04 amd64
openjdk-17-jdk/jammy-updates,jammy-security 17.0.13+11-2ubuntu1~22.04 amd64
openjdk-18-jdk/jammy-updates,jammy-security 18.0.2+9-2~22.04 amd64
openjdk-19-jdk/jammy-updates,jammy-security 19.0.2+7-0ubuntu3~22.04 amd64
openjdk-21-jdk/jammy-updates,jammy-security,now 21.0.5+11-1ubuntu1~22.04 amd64 [installed]
openjdk-8-jdk/jammy-updates,jammy-security 8u432-ga~us1-0ubuntu2~22.04 amd64
Nous utiliserons la version openjdk-21-jdk :
sudo apt install openjdk-21-jdk
Vous pouvez ensuite vérifier que Java est correctement installé en affichant sa version :
java --version
La sortie du terminal ressemblera à ceci :
openjdk 21.0.5 2024-10-15
OpenJDK Runtime Environment (build 21.0.5+11-Ubuntu-1ubuntu122.04)
OpenJDK 64-Bit Server VM (build 21.0.5+11-Ubuntu-1ubuntu122.04, mixed mode, sharing)
Comme indiqué, la version exacte d’OpenJDK est 21.0.5.
Tous les exemples de ce guide doivent être enregistrés dans un fichier séparé avec l’extension .java :
nano App.java
Remplissez ensuite le fichier créé avec un code d’exemple comme celui-ci :
class App {
public static void main(String[] args) {
System.out.println("This text is printed to the console");
}
}
Notez que le nom de la classe doit correspondre au nom du fichier.
Compilez ensuite le fichier :
javac App.java
Et exécutez-le :
java App
Le terminal affichera :
This text is printed to the console
Toutes les exceptions en Java ont un type spécifique associé à la raison pour laquelle l’exception s’est produite — le type particulier d’erreur du programme.
Il existe deux types fondamentaux d’exceptions :
Le type Error n’est considéré comme une exception que de manière conditionnelle — c’est une erreur complète qui entraîne inévitablement l’arrêt du programme.
Les exceptions qui peuvent être gérées par du code personnalisé et permettent au programme de continuer à s’exécuter sont les Checked Exceptions et les Unchecked Exceptions.
Ainsi, les erreurs et les exceptions en Java sont des entités différentes. Cependant, les deux — Errors et Exceptions (Checked et Unchecked) — sont des types avec des sous-types supplémentaires précisant la raison de l’échec.
Voici un exemple de code qui déclenche une exception au moment de la compilation :
import java.io.File;
import java.util.Scanner;
public class App {
public static void main(String[] args) {
File someFile = new File("someFile.txt"); // créer une référence de fichier
Scanner scanner = new Scanner(someFile); // analyser le contenu du fichier
}
}
La compilation sera interrompue et vous verrez l’erreur suivante dans le terminal :
App.java:7: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
Scanner scanner = new Scanner(someFile);
^
1 error
Si vous capturez et gérez cette exception, le code se compilera et pourra être exécuté correctement.
Voici un autre exemple de code qui déclenche une exception uniquement à l’exécution :
class App {
public static void main(String[] args) {
int[] someArray = {1, 2, 3, 4, 5}; // créer un tableau avec 5 éléments
System.out.println(someArray[10]); // tenter d’accéder à un élément inexistant
}
}
Aucune exception ne se produira lors de la compilation, mais après avoir exécuté le code compilé, vous verrez cette erreur dans le terminal :
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 5
at app.main(app.java:4)
Cela signifie qu’une telle exception peut être gérée par du code défini par l’utilisateur, permettant au programme de continuer son exécution.
Enfin, voici un exemple de code qui provoque une erreur d’exécution :
public class App {
static int i = 0;
public static int showSomething(int x) {
i = i + 2;
return i + showSomething(i + 2);
}
public static void main(String[] args) {
App.showSomething(i); // provoquer un dépassement de pile (stack overflow)
}
}
La compilation réussira, mais pendant l’exécution, le terminal affichera un StackOverflowError :
Exception in thread "main" java.lang.StackOverflowError
at java.base/java.io.BufferedOutputStream.implWrite(BufferedOutputStream.java:220)
at java.base/java.io.BufferedOutputStream.write(BufferedOutputStream.java:200)
at java.base/java.io.PrintStream.implWrite(PrintStream.java:643)
Dans ce cas, l’erreur ne peut pas être gérée ; elle ne peut être corrigée que dans le code.
En interne, toutes les exceptions (et erreurs) en Java sont représentées sous forme d’un ensemble de classes, dont certaines héritent d’autres.
La classe de base pour toutes les erreurs et exceptions est Throwable. Deux autres classes héritent d’elle : Error et Exception, qui servent de classes de base pour un large éventail de sous-classes associées à des types d’exceptions spécifiques.
La classe Error décrit les erreurs de type erreur, comme mentionné précédemment, tandis que la classe Exception décrit les exceptions vérifiées.
De plus, la classe RuntimeException hérite de Exception et décrit les exceptions non vérifiées.
Une hiérarchie simplifiée des classes d’exceptions Java peut être représentée ainsi :
Chaque classe d’exception contient des méthodes pour obtenir des informations supplémentaires sur l’erreur.
Vous pouvez trouver la classification complète des exceptions Java, y compris celles des packages supplémentaires, dans un guide de référence dédié.
Toutes les exceptions sont gérées à l’aide de blocs spéciaux try et catch, qui sont standard dans la plupart des langages de programmation, y compris Java.
À l’intérieur du bloc try, vous écrivez le code susceptible de contenir une erreur pouvant générer une exception.
À l’intérieur du bloc catch, vous écrivez le code qui gère l’exception survenue dans le bloc try précédemment défini.
Par exemple, une structure try-catch peut ressembler à ceci :
public class App {
public static void main(String[] args) {
try {
// code that might throw an exception
int someVariable = 5 / 0;
System.out.println("Who said you can’t divide by zero?");
} catch (ArithmeticException someException) {
// code that handles the exception
System.out.println("Actually, you can't divide by zero...");
}
}
}
La sortie de ce code dans la console sera :
Actually, you can't divide by zero...
Cet exemple est basé sur une division illégale par zéro, incluse dans un bloc try et lançant une ArithmeticException.
Dans le bloc catch, cette exception est gérée en affichant un message d’erreur dans la console.
Grâce à cette structure, le programme peut continuer à s’exécuter même après l’erreur de division par zéro.
Contrairement à de nombreux autres langages de programmation, Java inclut un bloc spécial finally dans son mécanisme de gestion des exceptions. Il s’exécute toujours, qu’une exception se soit produite ou non.
Nous pouvons donc étendre la structure précédente :
public class App {
public static void main(String[] args) {
try {
// code that might throw an exception
int someVariable = 5 / 0;
} catch (ArithmeticException someException) {
// code that handles the exception
System.out.println("Actually, you can't divide by zero...");
} finally {
// code that always executes
System.out.println("Who cares if you can divide by zero or not? This message will appear anyway!");
}
}
}
Après exécution de ce code, la console affichera :
Actually, you can't divide by zero...
Who cares if you can divide by zero or not? This message will appear anyway!
Pour comprendre l’utilité pratique du bloc finally, considérez l’exemple suivant :
try {
parseJson(response.json);
} catch (JSONException someException) {
System.out.println("Looks like there’s something wrong with the JSON...");
}
// a function that hides the loading indicator
hideLoaderUI();
Dans un programme utilisant cette structure, la fonction hideLoaderUI() ne s’exécutera jamais si une exception se produit.
Vous pourriez essayer d’appeler hideLoaderUI() à la fois à l’intérieur du gestionnaire d’exceptions et ensuite :
try {
parseJson(response.json);
} catch (JSONException someException) {
hideLoaderUI(); // duplicate
System.out.println("Looks like there’s something wrong with the JSON...");
}
hideLoaderUI(); // doublon
Cependant, cela entraîne une duplication indésirable de l’appel de fonction. De plus, il peut s’agir d’un bloc complet de code, et dupliquer un tel code est considéré comme une mauvaise pratique.
Ainsi, pour garantir l’exécution de hideLoaderUI() sans duplication, vous pouvez utiliser un bloc finally :
try {
parseJson(response.json);
} catch (JSONException someException) {
System.out.println("Looks like there’s something wrong with the JSON...");
} finally {
// s’exécutera dans tous les cas
hideLoaderUI();
}
Java vous permet de créer (lancer) des exceptions manuellement à l’aide de l’opérateur spécial throw :
public class App {
public static void main(String[] args) {
throw new Exception("Something strange seems to have happened...");
}
}
Vous pouvez même créer une variable pour l’exception à l’avance et la lancer ensuite :
public class App {
public static void main(String[] args) {
var someException = new Exception("Something strange seems to have happened...");
throw someException;
}
}
Un autre mot-clé important, throws (avec un "s" à la fin), permet de déclarer explicitement les types d’exceptions (sous forme de noms de classes) qu’une méthode peut lancer.
Si une telle méthode lance une exception, elle sera transmise au code appelant, qui devra la gérer :
public class App {
public static void someMethod() throws ArithmeticException, NullPointerException, InterruptedException {
int someVariable = 5 / 0;
}
public static void main(String[] args) {
try {
App.someMethod();
} catch (Exception someException) {
System.out.println("Dividing by zero again? Do you even know what insanity is?");
}
}
}
La sortie de la console sera :
Dividing by zero again? Do you even know what insanity is?
La structure hiérarchique des exceptions permet naturellement de créer des classes d’exceptions personnalisées qui héritent des classes de base.
Grâce aux exceptions personnalisées, Java permet d’implémenter des chemins de gestion d’erreurs spécifiques à une application.
Ainsi, en plus des exceptions standard de Java, vous pouvez en ajouter de nouvelles.
Chaque exception personnalisée, comme toute exception prédéfinie, peut être gérée à l’aide des blocs standards try-catch-finally :
class MyOwnException extends Exception {
public MyOwnException(String message) {
super(message); // appelle le constructeur de la classe parente
System.out.println("Warning! An exception is about to be thrown!");
}
}
public class App {
public static void main(String[] args) {
try {
throw new MyOwnException("Just an exception. No explanation. Anyone got a problem?");
} catch (MyOwnException someException) {
System.out.println(someException.getMessage());
}
}
}
Sortie de la console :
Warning! An exception is about to be thrown!
Just an exception. No explanation. Anyone got a problem?
Ce tutoriel a démontré, à travers des exemples, pourquoi les exceptions sont nécessaires en Java, comment elles apparaissent (y compris comment en lancer une manuellement) et comment les gérer à l’aide des outils du langage.
Les exceptions qui peuvent être capturées et gérées existent en deux types :
Checked Exceptions : gérées à la compilation.
Unchecked Exceptions : gérées à l’exécution.
En plus de celles-ci, il existe des erreurs fatales qui ne peuvent être corrigées qu’en réécrivant le code :
Errors : ne peuvent pas être gérées.
Plusieurs structures syntaxiques (blocs) sont utilisées pour gérer les exceptions :
try : code susceptible de lancer une exception.
catch : code qui gère l’exception possible.
finally : code qui s’exécute toujours, qu’une exception se soit produite ou non.
De même, des mots-clés permettent de contrôler le processus de lancement d’exceptions :
throw : Génère manuellement une exception.
throws : Répertorie les exceptions possibles dans une méthode déclarée.
Vous trouverez la liste complète des méthodes de la classe parente Exception dans la documentation officielle d'Oracle.