Exceptions in Java are a crucial part of robust programming. Indeed, they allow errors to be managed in an organized and predictable manner. This article explores the exception handling system in Java in depth, provides best practices, and includes real-world examples to illustrate the concepts covered.
In Java, an exception is an unexpected event that occurs during the execution of a program and disrupts the normal flow of instructions. Exceptions allow the program to handle errors without crashing.
Simple error example: division by zero:
public class DivisionParZero { public static void main(String[] args) { int a = 10; int b = 0; System.out.println(a / b); // Lève une ArithmeticException } }
Java classifies exceptions into three main categories:
These exceptions must be caught or declared in the method signature with throws. They often come from external events like file access or network connections.
Unchecked exceptions inherit from RuntimeException and do not need to be caught or declared. They often result from programming errors.
Errors in Java are serious, often system-related problems that the program usually cannot handle.
These exceptions cover a wide range of possible errors in Java, providing a comprehensive basis for handling various error scenarios within an application.
In Java, exception handling is mainly based on the try, catch, finally, and throw blocks. Here is a detailed overview of their use:
The try block encapsulates code that can generate an exception. If an exception occurs, the corresponding catch block is executed to catch and handle this exception, preventing the program from crashing.
Example:
Let's imagine a scenario where we want to convert a string to an integer, but the string might not be a valid number.
public class DivisionParZero { public static void main(String[] args) { int a = 10; int b = 0; System.out.println(a / b); // Lève une ArithmeticException } }
In this example, if the user enters a character string that cannot be converted to an integer, such as "abc", the catch block catches NumberFormatException and displays an error message, thus avoiding a program interruption.
The finally block executes after the try and catch blocks, whether or not an exception was thrown. It is often used to free up resources (for example, closing files or network connections) to ensure that resources are always cleaned up properly.
Example:
public class GestionDesExceptions { public static void main(String[] args) { try { int number = Integer.parseInt("abc"); // Provoquera une NumberFormatException } catch (NumberFormatException e) { System.out.println("Erreur : la chaîne de caractères n'est pas un nombre valide."); } } }
Here, the finally block closes the file, whether it was found or not, to free the resources.
The throw keyword is used to explicitly throw an exception. This can be useful for specific validations in the code.
Example:
public class GestionDesExceptions { public static void main(String[] args) { FileReader fr = null; try { fr = new FileReader("fichier.txt"); // Lire le fichier } catch (FileNotFoundException e) { System.out.println("Erreur : fichier non trouvé."); } finally { if (fr != null) { try { fr.close(); } catch (IOException e) { System.out.println("Erreur lors de la fermeture du fichier."); } } } } }
In this example, throw explicitly throws an IllegalArgumentException if the age is negative, thus indicating a logical error.
Sometimes it is better to propagate an exception instead of catching it immediately, especially if the current method cannot handle the exception properly. This is done with the throws keyword in the method declaration.
Example:
public class GestionDesExceptions { public static void main(String[] args) { try { validerAge(-5); } catch (IllegalArgumentException e) { System.out.println("Erreur : " + e.getMessage()); } } public static void validerAge(int age) { if (age < 0) { throw new IllegalArgumentException("L'âge ne peut pas être négatif."); } } }
Here, openFile uses throws IOException, which means it lets the caller handle the exception. If FileReader cannot find the file, a FileNotFoundException will be thrown and passed to the main method's catch block.
This propagation is useful in more complex architectures where exceptions need to be handled higher in the call hierarchy, for example, at the presentation layer to display an error message to the user.
Java allows you to create custom exceptions by inheriting from the Exception or RuntimeException class. These exceptions are useful for reporting application-specific errors that are not covered by standard exceptions.
Suppose we create a banking app where the user cannot withdraw more money than they have. We will create an InsufficientBalanceException.
import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; public class PropagationException { public static void main(String[] args) { try { ouvrirFichier("fichier_inexistant.txt"); } catch (IOException e) { System.out.println("Erreur lors de l'ouverture du fichier : " + e.getMessage()); } } public static void ouvrirFichier(String nomFichier) throws IOException { FileReader lecteur = new FileReader(nomFichier); // Peut lever FileNotFoundException lecteur.close(); } }
Then we use it in our main program to handle situations where the balance is insufficient:
public class SoldeInsuffisantException extends Exception { public SoldeInsuffisantException(String message) { super(message); } }
In this example, InsufficientBalanceException is a custom exception indicating that the amount to be withdrawn exceeds the available balance. Handling this error specifically improves code readability and maintainability by providing explicit error messages.
Let's imagine a user validation system where the age must be between 18 and 65 years old. We can create an AgeInvalideException.
public class DivisionParZero { public static void main(String[] args) { int a = 10; int b = 0; System.out.println(a / b); // Lève une ArithmeticException } }
Usage:
public class GestionDesExceptions { public static void main(String[] args) { try { int number = Integer.parseInt("abc"); // Provoquera une NumberFormatException } catch (NumberFormatException e) { System.out.println("Erreur : la chaîne de caractères n'est pas un nombre valide."); } } }
Here, AgeInvalideException is thrown if the age does not meet the conditions. This custom exception provides precise validation checks and clear error messages, improving user experience.
Exception handling in Java through try, catch, finally, and custom exceptions allows fine-grained error control, better code readability, and a better user experience in professional applications.
Principle: Exception messages must be explicit to provide precise context about the error.
Bad Practice:
public class GestionDesExceptions { public static void main(String[] args) { FileReader fr = null; try { fr = new FileReader("fichier.txt"); // Lire le fichier } catch (FileNotFoundException e) { System.out.println("Erreur : fichier non trouvé."); } finally { if (fr != null) { try { fr.close(); } catch (IOException e) { System.out.println("Erreur lors de la fermeture du fichier."); } } } } }
Improvement:
Use specific exceptions and clear messages, and prefer to create a custom exception if it reflects a common, domain-specific error.
public class GestionDesExceptions { public static void main(String[] args) { try { validerAge(-5); } catch (IllegalArgumentException e) { System.out.println("Erreur : " + e.getMessage()); } } public static void validerAge(int age) { if (age < 0) { throw new IllegalArgumentException("L'âge ne peut pas être négatif."); } } }
Exceptions should not override control structures like if-else. Using exceptions in this way is costly in terms of performance and makes the code less readable.
Bad Practice:
import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; public class PropagationException { public static void main(String[] args) { try { ouvrirFichier("fichier_inexistant.txt"); } catch (IOException e) { System.out.println("Erreur lors de l'ouverture du fichier : " + e.getMessage()); } } public static void ouvrirFichier(String nomFichier) throws IOException { FileReader lecteur = new FileReader(nomFichier); // Peut lever FileNotFoundException lecteur.close(); } }
Improvement: Use normal control structures to handle this type of logic.
public class SoldeInsuffisantException extends Exception { public SoldeInsuffisantException(String message) { super(message); } }
The try-with-resources block ensures that resources, such as files or connections, are closed automatically, even in the event of an exception.
Bad Practice:
public class CompteBancaire { private double solde; public CompteBancaire(double solde) { this.solde = solde; } public void retirer(double montant) throws SoldeInsuffisantException { if (montant > solde) { throw new SoldeInsuffisantException("Solde insuffisant pour ce retrait."); } solde -= montant; } public static void main(String[] args) { CompteBancaire compte = new CompteBancaire(100.0); try { compte.retirer(150.0); } catch (SoldeInsuffisantException e) { System.out.println(e.getMessage()); } } }
Improvement:
public class AgeInvalideException extends Exception { public AgeInvalideException(String message) { super(message); } }
Avoid catching general exceptions like Exception or Throwable, as this can hide critical errors and unexpected bugs. Catching specific exceptions makes the code more readable and maintainable.
Bad Practice:
public class ValidationUtilisateur { public static void validerAge(int age) throws AgeInvalideException { if (age < 18 || age > 65) { throw new AgeInvalideException("Âge invalide : doit être entre 18 et 65 ans."); } } public static void main(String[] args) { try { validerAge(17); } catch (AgeInvalideException e) { System.out.println(e.getMessage()); } } }
Improvement:Target specific exceptions for more precise error handling.
public class DivisionParZero { public static void main(String[] args) { int a = 10; int b = 0; System.out.println(a / b); // Lève une ArithmeticException } }
Exception logging makes it easier to track and resolve issues. Use a logging framework like Log4j or SLF4J to log errors, choosing appropriate logging levels (error, warn, info).
Bad Practice:
public class GestionDesExceptions { public static void main(String[] args) { try { int number = Integer.parseInt("abc"); // Provoquera une NumberFormatException } catch (NumberFormatException e) { System.out.println("Erreur : la chaîne de caractères n'est pas un nombre valide."); } } }
Improvement:
public class GestionDesExceptions { public static void main(String[] args) { FileReader fr = null; try { fr = new FileReader("fichier.txt"); // Lire le fichier } catch (FileNotFoundException e) { System.out.println("Erreur : fichier non trouvé."); } finally { if (fr != null) { try { fr.close(); } catch (IOException e) { System.out.println("Erreur lors de la fermeture du fichier."); } } } } }
Exception handling is more complex in asynchronous environments, such as with CompletableFuture, because errors can occur outside of the main execution flow.
Example:
public class GestionDesExceptions { public static void main(String[] args) { try { validerAge(-5); } catch (IllegalArgumentException e) { System.out.println("Erreur : " + e.getMessage()); } } public static void validerAge(int age) { if (age < 0) { throw new IllegalArgumentException("L'âge ne peut pas être négatif."); } } }
To rethrow an exception while preserving its initial context, use getCause(). This is particularly useful when handling exceptions in higher layers of the application.
Example:
import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; public class PropagationException { public static void main(String[] args) { try { ouvrirFichier("fichier_inexistant.txt"); } catch (IOException e) { System.out.println("Erreur lors de l'ouverture du fichier : " + e.getMessage()); } } public static void ouvrirFichier(String nomFichier) throws IOException { FileReader lecteur = new FileReader(nomFichier); // Peut lever FileNotFoundException lecteur.close(); } }
In this example, e is the initial cause and can be retrieved by getCause() for easier traceability.
Unit tests ensure that exceptions are properly thrown and handled. With JUnit, we can check that a method throws the expected exception.
Example:
public class SoldeInsuffisantException extends Exception { public SoldeInsuffisantException(String message) { super(message); } }
In this example, assertThrows checks that division throws an ArithmeticException in case of division by zero.
By following these best practices for exception handling, you can make your Java code more robust and maintainable. Good error handling not only guarantees the stability of the application, but it also improves error traceability, thus facilitating debugging and continuous improvement.
In summary, rigorous exception handling in Java strengthens code reliability and maintainability. By adopting best practices — such as explicit error messages, judicious use of try-with-resources, and attention to the readability and testability of exceptions — we avoid unnecessary interruptions and guarantee a more stable user experience. . This makes it possible to detect, understand and correct errors efficiently, while providing a robust code base ready for evolution.
The above is the detailed content of Exception Handling in Java: Complete Guide. For more information, please follow other related articles on the PHP Chinese website!