Exception Handling in JAVA

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:

  1. 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.

  2. 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:

  1. 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.

  2. 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.

  3. 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 the try 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:

    1. 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.

    2. 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.

    3. 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 or Error) are not subject to the same restriction. This is because unchecked exceptions represent programming errors (like NullPointerException, 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 a NullPointerException, 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 the RuntimeException 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 or RuntimeException. 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 exception CustomCheckedException.

    • The throwCustomUncheckedException() method throws a custom unchecked exception CustomUncheckedException.

    • 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());
        }
    }
}