Introduction to Exception Handling
What is an Exception?
An exception in Java is an event that disrupts the normal flow of the program's instructions during execution.
Types of Exceptions:
Checked Exceptions(Compile time): These are exceptions that the compiler forces you to handle. They are subclasses of Exception but not subclasses of RuntimeException.
Unchecked Exceptions(Runtime Exceptions): These are exceptions that are not checked at compile time. They are subclasses of RuntimeException and typically occur due to programming errors like dividing by zero or accessing an invalid index in an array.
Exception Handling Keywords:
try: This block is used to enclose the code that might throw an exception.
catch: This block follows a try block and catches exceptions thrown by the try block.
finally: This block, if present, is executed whether an exception is thrown or not, making it useful for cleanup tasks like closing resources.(finally even runs when return is encountered in try/catch block)
throws: Used to signal that a function , when used must handle a ducked exception
throw: This keyword is used to explicitly throw an exception.
That stack frame, where the exeception occurs, creates exception object and hands it over to JVM, in the same stack frame if custom handler is present , JVM will hand it over it that , if not it will be passed on to the parent stack frame, if not it will hand it to default exception handler
Default exception handler , leads to abnormal termination
If exception is handled by the handler in same stack frame , it's not propagated to parent stack anymore automatically unless exception if thrown from the custom handler itself- Rethrowing an exception
The Lines of code written AFTER the point where exception has occurred - WILL NOT BE RUN ie lines from point of exception to catch will not get executed so you are better off writing granular exceptions (will have to anticipate exceptions in the code)
If you want to run some block of code , even after exception has occurred you can use finally keyword, this will run even with try as it will with catch
Whether exception occurs or not finally block will always run
Catch blocks should be ordered from most specific to least specific
Use the finally block for cleanup tasks like closing files or releasing resources.
Exception ducking
Exception ducking in Java refers to a method declaring that it throws an exception without actually handling it internally, thereby passing the responsibility of handling the exception to the calling code. Here are the pros and cons of this approach:
Pros:
Simplifies Method Logic: Ducking exceptions can simplify the logic within a method by allowing it to focus on its primary functionality without getting bogged down by exception handling code.
Flexibility for Callers: By declaring checked exceptions with the
throws
keyword, the method gives flexibility to callers to decide how to handle the exceptions based on their specific context.
Cons:
Passing the Buck: It shifts the burden of handling exceptions to the calling code, potentially leading to a chain of methods all declaring
throws
and passing the responsibility up the call stack.Less Predictable Behavior: Ducking exceptions can make the behavior of a method less predictable, as callers may not anticipate all possible exceptions that could be thrown and may not handle them appropriately.
Maintenance Challenges/Scalability: It can introduce maintenance challenges, as changes in exception handling within a method may require corresponding changes in all calling code.
public class DivideByZeroExample {
public static void main(String[] args) {
try {
int result = divideNumbers(10, 0);
// INVOKER FUNCTION HANDLING DUCKED EXCEPTION
System.out.println("Result: " + result);
} catch (ArithmeticException e) {
System.out.println("ArithmeticException occurred: " + e.getMessage());
}
}
public static int divideNumbers(int dividend, int divisor) throws ArithmeticException {
return dividend / divisor;
}
}
Finally will run even after the return statement is encountered
but finally cannot dominate System.exit(0)
NOTE: "try-with-resources" statement was introduced in Java 7 to simplify resource management, specifically the closing of resources that must be explicitly closed to avoid resource leaks, such as files, sockets, and database connections.
Why It Was Needed
Before try-with-resources, resource management required a lot of boilerplate code. Typically, developers had to use try-catch-finally
blocks to ensure resources were properly closed, leading to verbose and error-prone code. For example:
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("file.txt"));
// Use the reader
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
What It Does
The try-with-resources statement ensures that each resource is closed at the end of the statement. Any object that implements the java.lang.AutoCloseable
interface (which includes java.io
.Closeable
) can be used with try-with-resources. The code becomes cleaner and less error-prone:
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
// Use the reader
} catch (IOException e) {
e.printStackTrace();
}
How It Works
In a try-with-resources statement, you declare and initialize resources in the try
block. These resources are automatically closed at the end of the statement, regardless of whether an exception is thrown or not. The AutoCloseable
interface's close()
method is called on each resource.
Here’s a more detailed example with multiple resources:
try (
BufferedReader reader = new BufferedReader(new FileReader("file.txt"));
PrintWriter writer = new PrintWriter(new FileWriter("output.txt"))
) {
// Use the reader and writer
} catch (IOException e) {
e.printStackTrace();
}
In this example, both reader
and writer
are automatically closed when the try block exits, either normally or due to an exception.
Summary
Need: To reduce boilerplate code and errors associated with manually closing resources.
Function: Simplifies resource management by automatically closing resources declared in the try-with-resources statement.
Usage: Any object implementing
AutoCloseable
can be used, and resources are closed automatically at the end of thetry
block.
The try-with-resources statement makes code more readable, maintainable, and less error-prone, addressing a common source of bugs in Java applications
Hierarchy of Exception Object
Generally `able` suffixed entities inheriting from object as interfaces , but here Throwable is actually a class, so is Exception
Exceptions can be handled but not errors
Method Overriding with Exception handling- cannot throw `new` checked exception
In Java, when a method in a subclass overrides a method in a superclass, it can throw any unchecked exceptions but cannot throw new checked exceptions that were not declared in the overridden method of the superclass. This rule ensures that the contract of the superclass method is not broken. Here's why this rule exists:
Liskov Substitution Principle (LSP): According to the Liskov Substitution Principle, objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. If an overriding method in a subclass throws a checked exception that was not declared by the superclass method, it would break this principle. The client code that uses the superclass method would be forced to handle new exceptions it wasn't expecting, leading to potential runtime errors or forced changes in client code.
Exception Handling Consistency: When a method in a subclass overrides a method in the superclass, it inherits the contract of that method, including the declared exceptions. Allowing a subclass to throw additional checked exceptions would require all client code that calls the superclass method to handle these new exceptions, which could be impractical and lead to extensive refactoring.
Interface Implementation: Similar to method overriding, when a class implements an interface, the implemented methods cannot throw additional checked exceptions that are not declared in the interface. This ensures that any class implementing the interface can be used interchangeably without unexpected exceptions.
Consider the following superclass and subclass:
class Superclass {
public void method() throws IOException {
// Implementation
}
}
class Subclass extends Superclass {
@Override
public void method() throws FileNotFoundException {
// Implementation
}
}
In this example, FileNotFoundException
is a subclass of IOException
, so the overriding method can throw FileNotFoundException
because it is a subset of the declared IOException
in the superclass method. However, it cannot throw any other checked exceptions that are not declared by the superclass method.
You can even choose NOT to throw exception in the overriding fn
What About Unchecked Exceptions?
Unchecked exceptions (subclasses of
RuntimeException
orError
) are not subject to the same restriction. This is because unchecked exceptions represent programming errors (likeNullPointerException
,IndexOutOfBoundsException
, etc.) that are typically not expected to be caught explicitly:class Superclass { public void method() { // Implementation } } class Subclass extends Superclass { @Override public void method() throws NullPointerException { // Allowed // Implementation } }
Here, the
Subclass
method can throw aNullPointerException
, which is an unchecked exception. This is allowed because client code is not required to catch unchecked exceptions, and it does not alter the contract established by the superclass method.You can even choose NOT to throw exception in the overriding method even if parent method throws an exception
Custom Exceptions
What if you want to catch an `Password Mismatch` exception ?
Custom exceptions in Java are user-defined exceptions that extend either the
Exception
class (for checked exceptions-compile will ask us to handle them) or theRuntimeException
class (for unchecked exceptions). They allow developers to create specialized exception types tailored to their application's specific needs. Here's how you can create and use custom exceptions in Java:Creating a Custom Exception
You can create a custom exception by defining a new class that extends
Exception
orRuntimeException
. Here's an example of a custom checked exception:// Custom checked exception class CustomCheckedException extends Exception { public CustomCheckedException(String message) { super(message); } }
And here's an example of a custom unchecked exception:
// Custom unchecked exception class CustomUncheckedException extends RuntimeException { public CustomUncheckedException(String message) { super(message); } }
Throwing a Custom Exception
Once you've defined your custom exception, you can throw it using the
throw
keyword. Here's how you can throw your custom exceptions:public class CustomExceptionExample { // Method that throws a custom checked exception public void throwCustomCheckedException() throws CustomCheckedException { throw new CustomCheckedException("This is a custom checked exception."); } // Method that throws a custom unchecked exception public void throwCustomUncheckedException() { throw new CustomUncheckedException("This is a custom unchecked exception."); } public static void main(String[] args) { CustomExceptionExample example = new CustomExceptionExample(); try { example.throwCustomCheckedException(); } catch (CustomCheckedException e) { System.out.println("Caught CustomCheckedException: " + e.getMessage()); } try { example.throwCustomUncheckedException(); } catch (CustomUncheckedException e) { System.out.println("Caught CustomUncheckedException: " + e.getMessage()); } } }
In this example:
The
throwCustomCheckedException()
method throws a custom checked exceptionCustomCheckedException
.The
throwCustomUncheckedException()
method throws a custom unchecked exceptionCustomUncheckedException
.In the
main()
method, both methods are called within try-catch blocks to handle the exceptions.
import java.util.Scanner;
// Custom exception for underage applicants
class UnderageException extends Exception {
public UnderageException(String message) {
super(message);
}
}
// Custom exception for overage applicants
class OverageException extends Exception {
public OverageException(String message) {
super(message);
}
}
class Applicant {
int age;
public void input() {
Scanner scanner = new Scanner(System.in);
System.out.print("Enter age: ");
age = scanner.nextInt();
}
public void verify() throws UnderageException, OverageException {
if (age < 18) {
throw new UnderageException("Applicant is underage.");
} else if (age > 60) {
throw new OverageException("Applicant is overage.");
} else {
System.out.println("Congratulations, you are eligible.");
}
}
public static void main(String[] args) {
Applicant applicant = new Applicant();
applicant.input();
try {
applicant.verify();
} catch (UnderageException | OverageException e) {
System.out.println(e.getMessage());
}
}
}