Java 101: Exceptions to the programming rules, Part 2

Incorporate Java's throw-object/catch-object exception-handling technique into your Java programs

Last month's Java 101 article introduced you to exceptions. You learned that an exception diverges from normal execution and observed how C, C++, and Java programs handle exceptions. This month, I delve into Java's throw-object/catch-object exception-handling technique by focusing on that technique's exceptions class hierarchy, statements and clauses, and various rules. After completing this article, you will be able to incorporate Java's throw-object/catch-object exception-handling technique into your programs.

The exceptions class hierarchy

Java's throw-object/catch-object exception-handling technique includes an exceptions class hierarchy. Class Throwable resides at the top of that hierarchy, and the Exception and Error subclasses, which identify the hierarchy's program- and JVM-related exception class subhierarchies (respectively), reside immediately below -- as the figure below illustrates.

The exceptions class hierarchy organizes itself around three main classes

The exceptions class hierarchy associates with a StackTraceElement class, which lets you trace the method-call stack when an exception occurs. After exploring Throwable, Exception, and Error, I explore StackTraceElement and show how to extend the exceptions class hierarchy to suit your program's requirements.

The Throwable class

C++ lets any class represent an exception type. In contrast, Java permits only java.lang.Throwable and Throwable subclasses to represent those types. Apart from Object, an exception object's innermost layer is the Throwable layer. For that reason, you must familiarize yourself with the Throwable class.

Throwable possesses four constructors: Throwable(), Throwable(String message), Throwable(String message, Throwable cause), and Throwable(Throwable cause). The constructors reveal two facts about exception objects. First, you can associate a message with an exception object's Throwable layer. That message describes the exception's nature in human-readable terms, and a program can present that message to the program's user. Second, you can wrap one exception object inside another exception object. The wrapped exception object is called a cause because the wrapped object's presence causes an exception handler to create a second, different exception object that wraps itself around the wrapped object. A detailed examination of Throwable's constructors shows the following:

  • Throwable() initializes an exception object's Throwable layer to no message and no cause
  • Throwable(String message) initializes the Throwable layer to the contents of the String object that message references (and no cause)
  • Throwable(String message, Throwable cause) initializes the Throwable layer to the specified message and cause
  • Throwable(Throwable cause) initializes the Throwable layer's message to the cause's message (if there is a message) and the specified cause

Throwable also possesses eleven methods: getMessage(), getLocalizedMessage(), getCause(), initCause(Throwable cause), fillInStackTrace(), getStackTrace(), printStackTrace(), printStackTrace(PrintStream ps), printStackTrace(PrintWriter pw), setStackTrace(StackTraceElement [] stackTrace), and toString().

The getMessage() method returns a String reference to the Throwable layer's message. If that message does not exist, null returns. By default, getLocalizedMessage() calls getMessage() and passes that method's return value to whatever code calls getLocalizedMessage(). You can tailor getLocalizedMessage() to return the message in the user's written language by overriding that method in appropriate Throwable subclasses.

The getCause() method returns a reference to the Throwable layer's cause. In the case of no cause, getCause() returns null. The initCause(Throwable cause) method provides an alternate approach to initializing the cause. That method initializes the cause to the reference in that method's cause argument.

Tip
Not all legacy exception classes contain constructors that take cause arguments. If you need to store a cause in some exception object's Throwable layer and no constructor takes a cause argument, call that object's inherited initCause(Throwable cause) method. Remember, you can call that method only once. Furthermore, initCause(Throwable cause) throws a java.lang.IllegalArgumentException object if a reference to the calling object passes as an argument via cause. Finally, initCause(Throwable cause) throws a java.lang.IllegalStateException object if a call has already been made to that method, the Throwable(String message, Throwable cause) constructor, or the Throwable(Throwable cause) constructor.

Throwable dedicates six methods to working with a stack trace -- a sequence of method-call stack frames (where each stack frame describes an incomplete method call) and related source code information that identify the location of code responsible for throwing an exception object. fillInStackTrace() records the current stack trace in a private Throwable field. (Various Throwable methods call fillInStackTrace() behind the scenes.) The getStackTrace() method returns the contents of that field, which is an array of StackTraceElement objects. An exception handler can access that array and call StackTraceElement methods to help recover from an exception. (I discuss StackTraceElement's methods later in this article.) The three printStackTrace() methods print the stack-trace information in a suitable format on the standard error stream, a PrintStream stream, or a PrintWriter writer. (I discuss streams and writers in a future article.) Method setStackTrace(StackTraceElement[] stackTrace) lets you replace the current stack trace with a new stack trace, which stackTrace specifies.

Finally, Throwable overrides toString() to return a brief exception object description. You will see examples of toString() and other Throwable methods later in this article.

Exception and its children

Throwable's java.lang.Exception subclass serves as the root class of Throwable's program-related exception class subhierarchy. To describe an exception that originates from a program, such as a failed attempt to write to a file or an attempt to access a nonexistent array element, either the program or the JVM creates an object from a class in that subhierarchy. Examine the following list of exception class names and descriptions to familiarize yourself with the Exception subhierarchy's classes:

  • java.lang.CloneNotSupportedException: This class's objects describe attempts to call the clone() methods on objects whose classes do not directly (or indirectly, through superclasses) implement the Cloneable interface
  • java.io.FileNotFoundException: A java.io.IOException subclass, this class's objects describe failed attempts to open files
  • java.lang.InterruptedException: This class's objects describe interruptions to sleeping or waiting threads (which I'll discuss next month)
  • java.lang.ArrayIndexOutOfBoundsException: This class's objects describe attempts to access nonexistent array elements
  • java.lang.ArithmeticException: This class's objects describe attempts to divide integers by integer value zero
  • java.lang.NullPointerException: This class's objects describe attempts to access object fields or call object methods via object reference variables that contain null values

The previous list's first three classes represent different kinds of checked exceptions -- exceptions that a program must handle. To ensure the program handles the exceptions, the compiler checks the source code. In contrast, the list's last three classes represent different unchecked exceptions -- exceptions that a program doesn't need to handle. The compiler doesn't need to verify unchecked exception handling.

Note
Exception's java.lang.RuntimeException subclass and all RuntimeException subclasses identify different unchecked exception types. The JVM usually creates objects from those classes. All other Exception classes, including Exception, identify different checked exception types, and program code creates objects from those classes.

Programs must handle checked exceptions because programs can't guarantee such exceptions won't occur. How can a program guarantee it can write to a file?

The compiler does not require programs to handle unchecked exceptions for the following reasons:

  • Unchecked exceptions originate in many parts of a program; stating a program's intention to handle unchecked exception objects in all those places would overwhelm developers
  • Unless you build a library where someone else's program could possibly introduce a failure into your code, unchecked exceptions should never occur

Keep in mind that Exception subclass names end with the word Exception. That convention distinguishes Exception subclasses from Error subclasses.

Tip
Never create objects from the Exception class because an exception handler can't distinguish between the different exceptions those objects represent. (Operator instanceof proves useless in such situations.) To avoid that problem, always create exception objects from appropriate Exception subclasses (except for RuntimeException). The subclass's name tells you if that class is appropriate. For example, use FileNotFoundException to describe exceptions that arise from not finding files.

Error and its children

Throwable's java.lang.Error subclass serves as the root class of Throwable's JVM-related exception class subhierarchy. To describe an exception that originates from the JVM, such as the JVM running out of memory, the JVM creates an object from a class in that subhierarchy. JVM exceptions prove serious and often terminate the JVM.

I use the term error to distinguish JVM-related failure from program-related failure. Examples of error classes include java.lang.AssertionError, java.lang.ThreadDeath, and java.lang.VirtualMachineError. Not all error classes end with Error.

JVM errors belong to the unchecked exception category because a program can do little, if anything, to fix a JVM error. Also, don't create your own Error subclasses or try to handle JVM errors. The resulting program will probably prove unstable.

The StackTraceElement class

When your code must access stack-trace data (file names, source line numbers, method names, and so forth), that code can call Throwable's getStackTrace() method. The getStackTrace() method returns an array of references to java.lang.StackTraceElement objects -- where each object describes one stack frame in the stack trace.

Code can call StackTraceElement methods to access stack-trace data. Methods include getClassName(), getFileName(), getLineNumber(), getMethodName(), and isNativeMethod(). Each method returns information relevant to the stack frame associated with the StackTraceElement object.

What does stack-trace data look like? If you work with Sun Microsystems' SDK tools in a command-line environment, you probably see the stack-trace data produced by the printStackTrace() methods. If not, let's see if we can create some stack-trace data:

Listing 1. SortIntegerArray1.java

// SortIntegerArray1.java
class SortIntegerArray1
{
   public static void main (String [] args)
   {
      int numbers [] = { 22, 13, 5, 76, 3 };
      sort (numbers);
      for (int i = 0; i < numbers.length; i++)
           System.out.println (numbers [i]);
   }
   static void sort (int [] x)
   {
      for (int i = 0; i < x.length-1; i++)
           for (int j = 0; j < x.length-i; j++)
                if (x [j] > x [j+1])
                {
                    int temp = x [j];
                    x [j] = x [j+1];
                    x [j+1] = temp;
                }
   }
}

SortIntegerArray1 attempts to sort (order) a small integer array's contents into ascending order. However, the source code features a problem. After compiling that code and running the resulting class file, you encounter the following stack-trace data:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException
        at SortIntegerArray1.sort(SortIntegerArray1.java:19)
        at SortIntegerArray1.main(SortIntegerArray1.java:9)

The stack-trace data identifies ArrayIndexOutOfBoundsException as the thrown exception object. Because ArrayIndexOutOfBoundsException signifies an unchecked exception arising from flawed code, the JVM creates an object from that class (behind the scenes) and searches for an appropriate exception handler. Although the program can provide that exception handler and receive the exception object, it chooses not to do so. When a program doesn't receive an exception object, the JVM's default exception handler accepts that responsibility: it receives that object and calls printStackTrace() to print a stack trace -- which is what you see above.

The two lines that start with at refer to a pair of stack frames on the method-call stack. Each stack frame describes a method that has not completed its execution when an exception occurs. From the previous output, you see that neither main() nor sort() (which main() called) is complete. Furthermore, you see the corresponding source code lines where main() called sort() and where sort()'s attempt to access an invalid array element led to the exception.

1 2 3 4 5 Page 1