Java is a powerful and versatile programming language, but like any software, it’s not immune to errors and unexpected situations. In Java, exceptions are a mechanism that allows developers to handle such situations gracefully.
An exception is an event, which occurs during the execution of a program, that disrupts the normal flow of the program’s instructions.
When an error occurs within a method, the method creates an object and hands it off to the runtime system. The object, called an exception object, contains information about the error, including its type and the state of the program when the error occurred. Creating an exception object and handing it to the runtime system is called throwing an exception.
After a method throws an exception, the runtime system attempts to find something to handle it. The set of possible “somethings” to handle the exception is the ordered list of methods that had been called to get to the method where the error occurred. The list of methods is known as the call stack as shown below:
The runtime system scans the call stack to locate a method containing a code block capable of managing the exception; this block is referred to as an exception handler. The search initiates from the method where the error originated and progresses through the call stack in the reverse order of method calls. Once a suitable handler is identified, the runtime system transfers the exception to that handler. A handler is deemed appropriate if the type of the thrown exception object matches the type manageable by the handler.
The chosen exception handler is said to catch the exception. If the runtime system exhaustively examines all methods on the call stack without discovering a fitting exception handler, leading to a scenario depicted in the subsequent illustration, the runtime system (and consequently, the program) concludes abruptly.
There are three main types of exceptions in Java: checked exceptions, unchecked exceptions, and errors.
1. Checked exceptions are exceptions that are declared in the throws clause of a method declaration. The compiler checks whether the method properly handles the potential exceptions or whether the calling method is prepared to handle them. If a checked exception is thrown from a method that does not declare it, the calling method will be terminated with an error.
Common checked exceptions include:
2. Unchecked exceptions, known as RuntimeException is a special case: it’s unchecked (i.e. it need not be declared by a method and the compiler doesn’t force you to catch it). The compiler does not check for unchecked exceptions, and they can be thrown from any method, regardless of whether it declares them or not.
Common unchecked exceptions include:
3. Errors is the “rare” case: it signifies problems that are outside the control of the usual application: JVM errors, out of memory, problems verifying bytecode: these are things that you should not handle because if they occur things are already so bad that your code is unlikely to be able to handle it sanely.
Common errors include:
The Throwable
class is the superclass of all exceptions and errors in Java. All other exception classes are subclasses of the Throwable. The exception class hierarchy in Java is as follows:
try {
FileInputStream fileInputStream = new FileInputStream("file.txt");
} catch (FileNotFoundException e) {
System.err.println("File not found: " + e.getMessage());
}
try {
BufferedReader reader = new BufferedReader(new FileReader("data.txt"));
while (!reader.ready()) {
System.out.println("Waiting for data...");
}
String line = reader.readLine();
System.out.println("Read line: " + line);
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
}
try {
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase");
Statement statement = connection.createStatement();
statement.executeUpdate("UPDATE customers SET name = 'John Doe' WHERE id = 1");
} catch (SQLException e) {
System.err.println("Error executing SQL statement: " + e.getMessage());
}
4. ParseException: This exception is thrown when there is an error parsing a string into a specific format. It is commonly encountered in parsers for XML, JSON, or other structured data formats.
try {
JSONParser parser = new JSONParser();
JSONObject object = (JSONObject) parser.parse(new FileReader("data.json"));
String name = object.getString("name");
System.out.println("Name: " + name);
} catch (ParseException e) {
System.err.println("Error parsing JSON: " + e.getMessage());
}
String text = null;
System.out.println(text.length()); // NullPointerException
int[] numbers = new int[5];
System.out.println(numbers[10]); // ArrayIndexOutOfBoundsException
3. ArithmeticException: This exception is thrown when an arithmetic operation results in an invalid value. Common examples include dividing by zero or performing modulo division with a negative divisor.
int result = 10 / 0; // ArithmeticException
OutOfMemoryError: This error occurs when the Java Virtual Machine (JVM) runs out of memory. This can happen due to excessive memory allocation or resource-intensive operations.
for (int i = 0; i < Integer.MAX_VALUE; i++) {
new Object(); // Excessive object creation leads to OutOfMemoryError
}
Custom exceptions, also known as user-defined exceptions, are classes that extend the Exception or RuntimeException class. They are used to handle specific errors or situations that are not covered by the standard Java exceptions.
To create a custom exception, follow these steps:
Declare constructor: Declare a constructor for your custom exception. The constructor should take the necessary parameters to capture information about the error. For example, you can include a message describing the error, a stack trace, or any other relevant data.
// Custom exception class
class InsufficientFundsException extends Exception {
public InsufficientFundsException() {
super("Insufficient funds in the account.");
}
public InsufficientFundsException(String message) {
super(message);
}
}
// Bank Account class
class BankAccount {
private String accountHolder;
private double balance;
public BankAccount(String accountHolder, double initialBalance) {
this.accountHolder = accountHolder;
this.balance = initialBalance;
}
public void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException();
}
balance -= amount;
System.out.println("Withdrawal successful. Remaining balance: " + balance);
}
}
// Example of using the custom exception
public class BankingApplication {
public static void main(String[] args) {
BankAccount account = new BankAccount("John Doe", 1000);
try {
account.withdraw(500);
account.withdraw(800); // This will throw InsufficientFundsException
} catch (InsufficientFundsException e) {
System.out.println("Caught an exception: " + e.getMessage());
}
}
}
In this example: The InsufficientFundsException class extends the Exception class and provides a default constructor with a predefined error message.
The BankAccount class has a withdraw method that simulates a withdrawal from the account. If the withdrawal amount exceeds the account balance, it throws the InsufficientFundsException.
In the main method, we create a BankAccount instance with an initial balance of $1000. We then attempt to withdraw $500 (successful) and $800 (which will throw InsufficientFundsException). The exception is caught, and an appropriate message is printed.