Java Fundamentals Tutorial: Exceptions

9. Exceptions

Exception Handling in Java

9.1. What are Exceptions?

  • Exceptions are events that occur during the execution of programs that disrupt the normal flow of instructions (e.g. divide by zero, array access out of bound, etc.).
  • In Java, an exception is an object that wraps an error event that occurred within a method and contains:

    • Information about the error including its type
    • The state of the program when the error occurred
    • Optionally, other custom information
  • Exception objects can be thrown and caught.

Exceptions are used to indicate many different types of error conditions.

  • JVM Errors:

    • OutOfMemoryError
    • StackOverflowError
    • LinkageError

System errors:

  • FileNotFoundException
  • IOException
  • SocketTimeoutException

    • Programming errors:
  • NullPointerException
  • ArrayIndexOutOfBoundsException
  • ArithmeticException

9.2. Why Use Exceptions?

  • Exceptions separate error handling code from regular code.

    • Benefit: Cleaner algorithms, less clutter
  • Exceptions propagate errors up the call stack.

    • Benefit: Nested methods do not have to explicitly catch-and-forward errors (less work, more reliable)
  • Exception classes group and differentiate error types.

    • You can group errors by their generalize parent class, or
    • Differentiate errors by their actual class
  • Exceptions standardize error handling.

In a traditional programming language like C, an error condition is usually indicated using an integer return value (e.g. -1 for out of memory). However, this practice:

  • Requires standardization on error codes (hard for large projects)
  • Makes functions hard to use because they must return actual values by reference
  • Requires programmers to check return error codes in every nested function call. This leads to cluttered source code and hard-to-follow logic
  • Does not allow for the state of the program to be easily captured on an error condition for later examination
  • Cannot be enforced, and programmers often ignore error conditions, which leads to security or stability issues in programs

9.3. Built-In Exceptions

  • Many exceptions and errors are automatically generated by the Java virtual machine.
  • Errors, generally abnormal situations in the JVM, such as:

    • Running out of memory
    • Infinite recursion
    • Inability to link to another class
  • Runtime exceptions, generally a result of programming errors, such as:

    • Dereferencing a null reference
    • Trying to read outside the bounds of an array
    • Dividing an integer value by zero

public class ExceptionDemo {

    public static void main (String[] args) {
        System.out.println(divideArray(args));
    }

    private static int divideArray(String[] array) {
        String s1 = array[0];
        String s2 = array[1];
        return divideStrings(s1, s2);
    }

    private static int divideStrings(String s1, String s2) {
        int i1 = Integer.parseInt(s1);
        int i2 = Integer.parseInt(s2);
        return divideInts(i1, i2);
    }

    private static int divideInts(int i1, int i2) {
        return i1 / i2;
    }
}

Compile and try executing the program as follows:

java ExceptionDemo 100 4
java ExceptionDemo 100 0
java ExceptionDemo 100 four
java ExceptionDemo 100

Note that we could have overloaded all divide*() methods (since they have different parameters), but this way the generated stack traces are easier to follow.

9.4. Exception Types

Figure 7. A Sample of Exception Types

A Sample of Exception Types

All exceptions and errors extend from a common java.lang.Throwable parent class. Only Throwable objects can be thrown and caught.

Other classes that have special meaning in Java are java.lang.Error (JVM Error), java.lang.Exception (System Error), and java.lang.RuntimeException (Programming Error).

public class java.lang.Throwable extends Object
            implements java.io.Serializable {
    public Throwable();
    public Throwable(String msg);
    public Throwable(String msg, Throwable cause);
    public Throwable(Throwable cause);
    public String getMessage();
    public String getLocalizedMessage();
    public Throwable getCause();
    public Throwable initCause(Throwable cause);
    public String toString();
    public void printStackTrace();
    public void printStackTrace(java.io.PrintStream);
    public void printStackTrace(java.io.PrintWriter);
    public Throwable fillInStackTrace();
    public StackTraceElement[] getStackTrace();
    public void setStackTrace(StackTraceElement[] stackTrace);
}

9.5. Checked vs. Unchecked Exceptions

  • Errors and RuntimeExceptions are unchecked — that is, the compiler does not enforce (check) that you handle them explicitly.
  • Methods do not have to declare that they throw them (in the method signatures).
  • It is assumed that the application cannot do anything to recover from these exceptions (at runtime).
  • All other Exceptions are checked — that is, the compiler enforces that you handle them explicitly.
  • Methods that generate checked exceptions must declare that they throw them.
  • Methods that invoke other methods that throw checked exceptions must either handle them (they can be reasonably expected to recover) or let them propagate by declaring that they throw them.

There is a lot of controversy around checked vs. unchecked exceptions.

Checked exceptions give API designers the power to force programmers to deal with the exceptions. API designers expect programmers to be able to reasonably recover from those exceptions, even if that just means logging the exceptions and/or returning error messages to the users.

Unchecked exceptions give programmers the power to ignore exceptions that they cannot recover from, and only handle the ones they can. This leads to less clutter. However, many programmers simply ignore unchecked exceptions, because they are by default un-recoverable. Turning all exceptions into the unchecked type would likely lead to poor overall error handling.

It is interesting to note that Microsoft’s C# programming language (largely based on Java) does not have a concept of checked exceptions. All exception handling is purely optional.

9.6. Exception Lifecycle

  • After an exception object is created, it is handed off to the runtime system (thrown).
  • The runtime system attempts to find a handler for the exception by backtracking the ordered list of methods that had been called.

    • This is known as the call stack.
  • If a handler is found, the exception is caught.

    • It is handled, or possibly re-thrown.
  • If the handler is not found (the runtime backtracks all the way to the main() method), the exception stack trace is printed to the standard error channel (stderr) and the application aborts execution.

For example:

java ExceptionDemo 100 0

Exception in thread "main" java.lang.ArithmeticException: / by zero
at ExceptionDemo.divideInts(ExceptionDemo.java:21)
at ExceptionDemo.divideStrings(ExceptionDemo.java:17)
at ExceptionDemo.divideArray(ExceptionDemo.java:10)
at ExceptionDemo.main(ExceptionDemo.java:4)

Looking at the list bottom-up, we see the methods that are being called from main() all the way up to the one that resulted in the exception condition. Next to each method is the line number where that method calls into the next method, or in the case of the last one, throws the exception.

9.7. Handling Exceptions

Figure 8. The `try-catch-finally` Control Structure

The `try-catch-finally` Control Structure

public class ExceptionDemo {

    public static void main (String[] args) {
       divideSafely(args);
    }

    private static void divideSafely(String[] array) {
        try {
            System.out.println(divideArray(array));
        } catch (ArrayIndexOutOfBoundsException e) {
            System.err.println("Usage: ExceptionDemo <num1> <num2>");
        } catch (NumberFormatException e) {
            System.err.println("Args must be integers");
        } catch (ArithmeticException e) {
            System.err.println("Cannot divide by zero");
        }
    }

    private static int divideArray(String[] array) {
        String s1 = array[0];
        String s2 = array[1];
        return divideStrings(s1, s2);
    }

    private static int divideStrings(String s1, String s2) {
        int i1 = Integer.parseInt(s1);
        int i2 = Integer.parseInt(s2);
        return divideInts(i1, i2);
    }

    private static int divideInts(int i1, int i2) {
        return i1 / i2;
    }
}

9.8. Handling Exceptions (cont.)

  • The try-catch-finally structure:

    try {
        // Code bock
    }
    catch (ExceptionType1 e1) {
        // Handle ExceptionType1 exceptions
    }
    catch (ExceptionType2 e2) {
        // Handle ExceptionType2 exceptions
    }
    // ...
    finally {
        // Code always executed after the
        // try and any catch block
    }
    • Both catch and finally blocks are optional, but at least one must follow a try.
    • The try-catch-finally structure can be nested in try, catch, or finally blocks.
    • The finally block is used to clean up resources, particularly in the context of I/O.
    • If you omit the catch block, the finally block is executed before the exception is propagated.
  • Exceptions can be caught at any level.
  • If they are not caught, they are said to propagate to the next method.

public class ExceptionDemo {

    public static void main (String[] args) {
       divideSafely(args);
    }

    private static void divideSafely(String[] array) {
        try {
            System.out.println(divideArray(array));
        } catch (ArrayIndexOutOfBoundsException e) {
            System.err.println("Usage: ExceptionDemo <num1> <num2>");
        }
    }

    private static int divideArray(String[] array) {
        String s1 = array[0];
        String s2 = array[1];
        return divideStrings(s1, s2);
    }

    private static int divideStrings(String s1, String s2) {
        try {
            int i1 = Integer.parseInt(s1);
            int i2 = Integer.parseInt(s2);
            return divideInts(i1, i2);
        } catch (NumberFormatException e) {
            return 0;
        }
    }

    private static int divideInts(int i1, int i2) {
        try {
            return i1 / i2;
        } catch (ArithmeticException e) {
            return 0;
        }
    }
}

9.9. Grouping Exceptions

  • Exceptions can be caught based on their generic type. For example:

    catch (RuntimeException e) {
        // Handle all runtime errors
    }
[Caution]Caution

Be careful about being too generic in the types of exceptions you catch. You might end up catching an exception you didn’t know would be thrown and end up “hiding” programming errors you weren’t aware of.

  • catch statements are like repeated else-ifs.

    • At most one catch bock is executed.
    • Be sure to catch generic exceptions after the specific ones.

public class ExceptionDemo {

    public static void main (String[] args) {
       divideSafely(args);
    }

    private static void divideSafely(String[] array) {
        try {
            System.out.println(divideArray(array));
        } catch (ArrayIndexOutOfBoundsException e) {
            System.err.println("Usage: ExceptionDemo <num1> <num2>");
        } catch (RuntimeException e) {
            System.err.println("Error result: 0");
        }
    }

    private static int divideArray(String[] array) {
        String s1 = array[0];
        String s2 = array[1];
        return divideStrings(s1, s2);
    }

    private static int divideStrings(String s1, String s2) {
        int i1 = Integer.parseInt(s1);
        int i2 = Integer.parseInt(s2);
        return divideInts(i1, i2);
    }

    private static int divideInts(int i1, int i2) {
        return i1 / i2;
    }
}

9.10. Throwing Exception

  • A method that generates an unhandled exception is said to throw an exception. That can happen because:

    • It generates an exception to signal an exceptional condition, or
    • A method it calls throws an exception
  • Declare that your method throws the exception using the throws clause.

    • This is required only for checked exceptions.
  • Recommended: Document (in JavaDoc) that the method @throws the exception and under what circumstances.
  • To generate an exceptional condition:

    • Create an exception of the appropriate type. Use predefined exception types if possible, or create your own type if appropriate.
    • Set the error message on the exception, if applicable.
    • Execute a throw statement, providing the created exception as an argument.

Some of the more commonly used exception types are:

NullPointerException
Parameter value is null where prohibited
IllegalArgumentException
Non-null parameter value is inappropriate
IllegalStateException
Object state is inappropriate for method invocation
IndexOutOfBoundsException
Index parameter is out of range
UnsupportedOperationException
Object does not support the method

For example:

/**
* Circle shape
*/
public class Circle implements Shape {
    private final double radius;
    /**
    * Constructor.
    * @param radius the radius of the circle.
    * @throws IllegalArgumentException if radius is negative.
    */
    public Circle(double radius) {
        if (radius < 0.0) {
            throw new IllegalArgumentException("Radius " + radius
                + " cannot be negative");
        }
        this.radius = radius;
    }
    …
}

9.11. Creating an Exception Class

  • If you wish to define your own exception:

    • Pick a self-describing *Exception class name.
    • Decide if the exception should be checked or unchecked.

      Checked
      extends Exception
      Unchecked
      extends RuntimeException
    • Define constructor(s) that call into super’s constructor(s), taking message and/or cause parameters.
    • Consider adding custom fields, accessors, and mutators to allow programmatic introspection of a thrown exception, rather than requiring code to parse an error message.

/**
 * Exception thrown to indicate that there is insufficient
 * balance in a bank account.
 * @author me
 * @version 1.0
 */
public class InsufficientBalanceException extends Exception {
    private final double available;
    private final double required;

    /**
     * Constructor.
     * @param available available account balance
     * @param required required account balance
     */
    public InsufficientBalanceException(double available, double required) {
        super("Available $"+available+" but required $"+required);
        this.available = available;
        this.required = required;
    }

    /**
     * Get available account balance
     * @return available account balance
     */
    public double getAvailable() {
        return available;
    }

    /**
     * Get required account balance
     * @return required account balance
     */
    public double getRequired() {
        return required;
    }

    /**
     * Get the difference between required and available account balances
     * @return required - available
     */
    public double getDifference() {
        return required - available;
    }
}

9.12. Nesting Exceptions

  • A Throwable object can nest another Throwable object as its cause.
  • To make APIs independent of the actual implementation, many methods throw generic exceptions. For example:

    • Implementation exception: SQLException
    • API exception: DataAccessException
    • API exception nests implementation exception
    • To get the original implementation exception, do apiException.getCause()

public Customer getCustomer(String id) throws DataAccessException {
    try {
        Connection con = this.connectionFactory.getConnection();
        try {
            String sql
                = "SELECT * FROM Customers WHERE CustomerID='"
                + id + "'";
            PreparedStatement stmt = con.prepareStatement(sql);
            try {
                ResultSet result = stmt.executeQuery(sql);
                try {
                    return result.next() ? this.readCustomer(result) : null;
                } finally {
                    result.close();
                }
            } finally {
                stmt.close();
            }
        } finally {
            con.close();
        }
    } catch (SQLException e) {
        throw new DataAccessException("Failed to get customer ["
                        + id + "]: " + e.getMessage(), e);
    }
}

Observe that the getCustomer() method signature does not say anything about customers being retrieved from a SQL-based RDBMS. This allows us to provide an alternative implementation, say based on a LDAP directory server or XML files.