10.1 Java Exception Handling

Before we dive into how best to handle exceptions in the Struts framework, you should have a picture in your mind of what actually occurs when a method throws an exception. An understanding of the processes taking place in the JVM when an exception occurs may enlighten you as to the importance of throwing exceptions for the right reason, as well as the importance of throwing the right exceptions. Because there is additional overhead for the JVM to handle an exception, you should always take care to use exceptions correctly.

10.1.1 Java Exceptions

In Java, exceptions are objects that are created when an abnormal condition, often referred to as an exception condition, occurs during the execution of an application. When a Java application throws an exception, it creates an object that is a descendant of java.lang.Throwable. The Throwable class has two direct subclasses: java.lang.Error and java.lang.Exception. Figure 10-1 shows a partial hierarchy tree for the Throwable class.

Figure 10-1. A partial class hierarchy for the Throwable class

figs/jstr_1001.gif

Space does not permit all of the descendants of the Throwable class to be shown, as there are more than 100 direct and indirect subclasses in the core Java library alone. Normally, members of the Exception branches of the tree are thrown to indicate abnormal conditions that can usually be handled by the application. All of the exceptions your Struts application creates and throws should be subclasses of the Exception class. The other branch of Throwable, the Error class and its descendants, is reserved for more serious problems that occur during an application's lifecycle. For example, if there's no more memory available for an application, an OutOfMemoryError will occur, and there's typically nothing a client can do about it. Therefore, clients generally don't worry about handling the subclasses of Error. In most cases, it's the JVM itself that throws instances of Error or its subclasses.

10.1.2 The Method Invocation Stack

The JVM uses a method invocation stack, also referred to as a call stack, to keep track of the succession of method invocations of each thread. The stack holds local information about each method that has been called, going all the way back to the original main( ) method of the application. When each new method is invoked, a new stack frame is pushed onto the top of the stack, and the new method becomes the executing method. The local state of each method is also saved with each stack frame. Figure 10-2 illustrates an example Java call stack.

Figure 10-2. An example of a Java method invocation stack

figs/jstr_1002.gif

When a Java method completes normally, the JVM pops the current method's stack frame from the stack and continues processing in the previous method where it left off. When an exception condition occurs, however, the JVM must find a suitable exception handler. It first checks to see if the current method catches the exception or one of its parent exceptions. If so, execution will continue in that catch clause. If the current method doesn't provide a catch clause to handle the exception raised, the JVM will start popping method frames off the call stack until it finds a handler for the exception or one of its parent exceptions. Eventually, if it pops all the way back to the main( ) method and still doesn't find a handler for the exception, the thread will terminate. If that thread is the main thread and there are no other non-daemon threads running, the application itself will terminate. If the JVM does find an exception handler along the way, that method frame will become the top of the stack and execution will continue from there.

It's important to know how the JVM handles exceptions because there is plenty going on underneath the hood when exceptions occur in your applications. It can be a lot of work for the JVM to locate an exception handler for a particular exception, especially if the handler is located far down the call stack. It's very important that you provide sufficient exception handlers at the appropriate levels. If you let exceptions go, they are likely to halt your application.

10.1.3 What About the throws Clause?

When determining the method signatures for classes that are part of an application, you should give as much attention to deciding which exceptions the methods will throw as to what the parameters are and what the return type is.

You might have heard of the concept "design by contract." The idea behind this principle is that the set of public methods that a class exposes represents a virtual contract between a client and the class itself. The client has certain obligations in the way in which it invokes the method, and there may also be requirements on the class itself as part of the contract.

When something abnormal occurs and an exception is thrown from a method in the class, the contract, in a sense, has been broken. The class is informing the client that it can't fulfill its terms of the contract. It's entirely up to the caller to decide how to handle the exception. This is why the throws clause of a method signature is so important—it forces a client to decide what it will do when one of these abnormal conditions occurs. However, as you'll see in the next section, all Java exceptions are not equal.

10.1.4 Checked and Unchecked Exceptions

Java exceptions can be separated into two distinct groups: checked and unchecked. A checked exception signals an abnormal condition that the client must handle. All checked exceptions must either be caught and handled within the calling method or be declared in the throws clause following the method signature. This is why they are called "checked." The compiler and the JVM will verify that all checked exceptions that can occur in a method are handled. The compiler and JVM don't care if unchecked exceptions are ignored, because these are exceptions that the client usually cannot handle anyway. Unchecked exceptions, such as java.lang.ClassCastException, are typically the result of incorrect logic or programming errors.

The determination of whether an exception is checked or unchecked is based simply on its location in the exception hierarchy. All classes that are descendants of the java.lang.Exception class, except for subclasses of RuntimeException , are checked exceptions; the compiler will ensure that they are either handled by the method or listed in the throws clause. RuntimeException and its descendants are unchecked exceptions, and the compiler will not complain about these not being listed in a throws clause for a method or being handled in a try/catch block. This is why they are referred to as "unchecked."

10.1.5 Performance Impact of Exception Handling

In general, wrapping your Java code with try/catch blocks doesn't have a significant performance impact on your applications. Only when exceptions actually occur is there a negative performance impact, which is due to the lookup the JVM has to perform to locate the proper handler for the exception. If the catch block for the exception is located in the same method, the impact is not so bad. However, the further down the call stack the JVM has to go to find the exception handler, the greater the impact becomes.

This is why you should use a try/catch block only for error conditions. You should never use exceptions for things such as controlling program flow. The following use of a try/catch block is probably fine, but it's getting very close to improper use of exception handling:

Double basePrice = null;
String basePriceStr = request.getParameter( "BASE_PRICE_AMOUNT" );
     
// Use a try/catch to make sure the value is a number
try{
  basePrice = Double.valueOf( basePriceStr );
}catch( NumberFormatException ex ){
  // The value could not be converted to a valid Double; set the default
  basePrice = ApplicationDefaults.DEFAULT_BASE_PRICE;
}

The previous code fragment shows a try/catch block determining an error condition and taking corrective action. The error condition is an invalid price value, and the corrective action is to assign a default value. There are other ways to determine whether a string is a valid Double value, but using this approach is fairly popular. Fortunately, the exception handler is located in the same method, and the JVM doesn't incur a large penalty for this occurrence.

Of course, rules are always somewhat subjective, and what is a valid reason to one developer may not be to another. You should be aware of the drawbacks and avoid using try/catch blocks other than for actual error conditions.