Peter Haggar is an Advisory Software Engineer with IBM in Research Triangle Park, NC. He can
be contacted at [email protected].
EXCEPTION HANDLING IS a very powerful and useful feature of the Java language.
With this power comes a degree of complexity that must be understood to effectively utilize
exception handling in Java programs. Java introduces some new concepts to the exception
handling model that make exceptions both easier and more difficult to use properly.
Exception handling is not the silver bullet it is sometimes made out to be. It will not
solve all your error handling problems. In fact, it will introduce new problems, unless
a well-rounded understanding of this technology is grasped. Too often with exception
handling, programmers are led to believe that mastering a few new keywords will solve
the error handling problems encountered with traditional error handling techniques.
Exception handling simply represents a different avenue for dealing with errors. While
exception handling is a step forward with regard to traditional error handling techniques,
it creates new issues and problems that must be dealt with. Failure to understand these
issues and understand the problems surrounding them can lead to the creation of additional
problems in your error handling code.
When describing exception handling, most literature stops with a cursory look at the mechanics.
What is truly needed to write solid Java code that utilizes exception handling properly is a
full understanding of the mechanics of Java and then information on some of the nuances of
Java exception handling. I examine the most interesting and least understood areas of Java
exception handling: nuances and side effects. I also look at when to use it, when not to use
it, how to use it effectively, and the impact of exception handling on the performance of your Java code.
THE NUANCES OF JAVA EXCEPTION HANDLING
All language features have their associated nuances. This is also true of exception handling in
Java. Let's look at a few of them.
Keyword finally
The finally keyword is probably the best addition to the Java exception
handling model over the C++ model. finally enables the execution of code
to occur whether an exception occurred or not. Usage of finally is very
good for maintaining the internal state of an object when an exception occurs as well as cleaning up
non-object resources.
finally blocks are fairly straightforward, but have some behavior that
can catch the unwitting developer off guard. finally blocks are always executed. More specifically,
they will be entered when code within a try block:
- throws an exception
- finishes normally
- executes a return statement
- executes a break statement
- executes a continue statement
Let's look at the ramifications of this.
Listing 1
contains code that has some peculiar behavior.
Executing this code reveals two obvious results and one not so obvious. This code simply calls
three methods, method1(), method2(), and method3(),
and prints out the values they return. The call at //3 to
method1() will result in 1 being printed. The
call at //4 to method2() results in
2 being printed. The catch block in method2() is
never executed because an exception is not thrown. However, the call to the
method3() at //5 will print
4! This is because finally blocks are always executed regardless of what
happens in a try block. In this case the value 3 is about to be returned at //1.
However, on the execution of the return statement, control is transferred to the finally block at
//2. This causes the return 4 statement to
execute, resulting in method3() returning the integer
4.
Developers think that when they execute a return statement, they immediately leave the method they are
executing. This is no longer true with the usage of finally.
(finally is also entered on a break or continue statement.) This particular
problem has the potential to lead to long debugging sessions if you are not aware of this side effect of
finally.
The throws Clause
Many view the throws clause in Java as a savior. Remember the
throws clause is where you have to list all of the checked exceptions that
can propagate from a method. The compiler makes sure you either catch checked exceptions in a method
or declare them in the throws clause of the method. The
throws clause is provided to alert all callers of your method of the
exceptions that it can generate. I think the throws clause is a useful
language feature as implemented and provides useful information to users of methods. However, there
is a side effect to it that needs to be considered as you are designing your Java code.
What happens when you are well into your coding of a fairly large project and you add a checked
exception to a low-level worker method. For the purpose of this discussion, a worker method is one
that performs a common task for many other methods in your system and is called from many, many
places in your code. Typical systems have many such methods. You have two choices:
- Catch the checked exception in this method and handle it there.
- Throw the exception from the worker method to allow the caller to handle it.
Depending on the circumstances of your system, the first option may not be viable. This
worker method may not have the means to resolve the exception itself. You
may have no other choice but to choose the second option and throw this exception. This may not be as
easy as it sounds.
Adding an exception to a throws clause of a method will effect every
method that calls it. All methods that now call this worker method have to change. They also have the
same two choices on what to do as listed above. If they decide to handle the exception, the exception
will not propagate any further. However, what if these methods do not have the means of handling the
exception? They too must rely on the second option and add the exception to their throws clause. After
doing this you may attempt to compile your code again. When the compiler is done spitting out error
messages, you may now realize what you are up against. Now, all methods that call all of the methods
you just changed, have the same two choices. The process will continue all the way back to
main(), assuming no intervening method handles the exception.
The moral of this story is not to add exception handling at the end of the development cycle. Your error
handling strategy should be designed in from the beginning. If, as is often the case, you run into
this situation, even with careful planning, you now know what you may be up against. The
throws clause is a useful and beneficial language feature, but can also be
painful if you're not careful.
Nested Exceptions
What happens during the processing of a catch or
finally block, after an exception has been thrown, when some of the code
contained within the catch or finally throws an
exception? In addition, can you have a try/catch/finally block nested within
a try, catch, or finally block? If so, what would be the flow through this type of code?
Listing 2 contains a code
example that answers these and other questions. Trace through the code and try to determine the
output of the code (which is shown in Table 1).
Table 1. Output from code in Listing 2. |
In main, calling nestingTest() |
In outer most try block |
In inner try block |
In inner catch block |
Exception caught: Exception thrown from inner try |
In inner finally's try block |
In inner finally's finally block |
In outer catch |
Exception caught: Exception thrown from inner finally's finally block |
In outer finally |
In main, caught exception: Exception thrown from inner finally's |
finally block |
The code in Listing 2 consists of various try/catch/finally blocks nested within
one another. You can place try/catch/finally, try/catch,
or try/finally blocks anywhere in your Java code. In addition, as is
demonstrated, these blocks can be nested within each other. A try/catch/finally
block can be placed inside of another try, catch, or
finally block. There are ramifications to this, the least of which is the
code can become somewhat confusing. The more serious side effect is that exceptions get lost or hidden
from the caller.
Listing 2 consists of the method main(), which calls a
method nestingTest() and catches an exception thrown from it at
//1. Method nestingTest() consists of an
outermost try/catch/finally block at //2, //12,
and //13. Inside of the try block at //2, we
have another try/catch/finally block at //3, //5,
and //7. Inside of the finally block at
//7, we have an additional try/finally block
at //8 and //10. Let's examine the flow through
this code.
On entering main(), we print a message and call method
nestingTest(). Once inside nestingTest(), we
enter the outermost try block at //2, print a message, and enter the
inner try block at //3 printing another message. We then immediately
throw an exception at //4. When an exception is thrown from a
try block, control is transferred out of the try
block to its corresponding catch block. If a catch
block doesn't exist, control will transfer to the corresponding finally block.
(Try blocks cannot exist without a corresponding catch
or finally block.) In this case, control is transferred to the
catch block at //5, and we print our message
indicating the exception we caught. We then throw another exception at //6.
Before we can leave this code, we must always execute the finally block.
We enter the finally block at //7, and
immediately enter yet another try block at //8.
We print a message and throw another exception at //9.
We then enter the finally block at //10, print
a message, and throw yet another exception at //11.
So far, four exceptions have been thrown, one each at //4, //6, //9,
and //11 and we have completed executing the outer
try block at //2. It is interesting to note
what happens next. We enter the catch block at //12,
print a message indicating the exception we caught, then rethrow the exception and enter the outermost
finally at //13 and print a message. When
the finally block at //13 finishes, control is
returned to the catch block in main() at
//1. Note that the exception we catch at //1 is
the exception thrown from //11. What happened to the exceptions thrown
from //4, //6, and //9? The exception
thrown at //4 was caught at //5 and not
rethrown. The exceptions thrown from //6 and //9
were not caught by any code, but what has happened to them? In short, they are lost. They are also
referred to as hidden. The exception thrown at //9 hid the one thrown
at //6. The exception thrown at //11 hid
both of them. What are the effects of hiding exceptions? For one, the original exception is lost
and the code processing the last exception thrown (//12 in Listing 2)
will not know about the previous exceptions thrown. This could be very bad, because the original
problems reported in the exceptions thrown at //6 and
//9 are not known.
Losing or otherwise hiding exceptions is not a good thing. What can or should you do about it? The
best way to deal with this is when throwing exceptions from the inside of a try,
catch, or finally block, wrap the existing exception inside of
any new exceptions you may throw. This way, the receiver of the new exception will have information
about all errors, and critical error information will not be lost.
The code in Listing 2 shows us that try/catch/finally blocks can be
arbitrarily nested. In addition, only one exception can propagate from a
try/catch/finally block even though many can be thrown from within it.
Remember that only the last exception thrown will be seen by the caller while others will
be hidden and lost.
WHEN TO USE AND WHEN NOT TO USE EXCEPTION HANDLING
Exception handling was devised as a robust replacement for traditional error handling techniques.
Some believe that exception handling should be used for any and all error conditions and thus
completely avoiding traditional error return code checking. I believe that exception handling
can be overused. It should not be used exclusively or for control flow as a replacement for the
if/else clause. I believe a more moderate approach is necessary
whereby exception handling is used in conjunction with traditional error handling techniques.
Let's look at an example of this: Listing 3
contains some Java code that uses traditional error handling. Notice at //1 we
are checking to see if the data value returned from the getData() method
call is non-zero. If it is zero, we assume we are at the end of the stream. This makes sense
and is intuitive.
Now let's change this code by taking a hard line on exception handling. We will not rely on return
code values as is done at //1, but use exceptions.
Listing 4
contains the code in Listing 3 with exception handling added.
Notice at //1 we are placing a try/catch block
around the call to the getData() method. getData()
will no longer return zero when the stream is empty, it will throw a
NoMoreDataException. Even though the code in Listing 4 is a bit uglier
than the code in Listing 3, some people believe that Listing 4 represents the way code should be written
with exceptions and that returning error codes should never be done. I disagree. I think Listing 3
is much more intuitive even though it relies on the older methods of dealing with errors or unexpected
results. I think exceptions can be overused and Listing 4 is a good example of it.
Exceptions should be used for conditions outside the expected behavior of the code. In the example
above, we expect to get to the end of the stream, therefore a simple zero return from the
getData() method is appropriate and intuitive. I don't think throwing an
exception in this case is wise as this is not an exceptional condition but an expected one. We would
not, however, expect that the stream is corrupted. That type of condition would warrant an exception
being generated. The point is not to use exceptions for all conditions, but to use them where it makes
sense, where exceptional conditions exist.
EXCEPTIONS AND PERFORMANCE
There are three areas of exceptions and performance to address. The first is the effect of throwing an
exception. The second area concerns the effects of having try/catch blocks
in your code even when no exceptions are thrown. The third area addresses the question: Can I write
faster Java code by throwing an exception? The answer to this may surprise you.
Ramifications of Throwing Exceptions
As some may believe, the act of throwing an exception is not free. After all, what are Java exceptions?
Java exceptions are objects and these objects need to be created. Creating objects is somewhat costly,
therefore, the act of throwing an exception has some cost to it. To throw an exception in Java you write
something like:
throw new MyException(...);
This creates a new object and then transfers control to one of multiple places. Because there is a cost
to throwing exceptions, I recommend exceptions be used only for error conditions and not for control
flow. See "Faster Code with Exceptions?" below for an exception (no pun intended) to this rule. The
reason for this is due to the object creation aspect of exception handling, which thereby makes
throwing exceptions inherently slow. When exceptions are used for error/failure conditions, I am
not too concerned with speed. I want my code to run fast when it's working and normally
don't care how long it takes to fail.
Another item to note is that the act of throwing an exception is analogous to a
jump or goto statement. When an exception is
thrown, program control is immediately transferred to one of three places:
- catch block
- finally block
- calling method
All of these options have one thing in common. The code that was executing at the time an exception is
thrown stops executing and control is transferred elsewhere. This has ramifications on the state and
validity of your objects.
Whenever discussing performance and Java you have to account for the Java virtual machine (JVM) you are
using and the operating system you are running on. I have seen large differences between JVMs I have
used. Therefore, for what I am about to discuss, I highly recommend you perform some level of profiling
on your systems first before assuming you will get the same results as indicated here.
Effects of try/catch Blocks
on Performance
Placing try/catch blocks in your Java code can actually slow it down,
even when exceptions are not thrown.
Listing 5
contains two methods, method1() and method2(),
at //1 and //2, that contain almost identical
code. Method1() does not contain any try/catch
blocks, while method2() contains the code of method1()
with the addition of a try/catch block in the body of the for loop at
//3. method2() doesn't actually ever throw any
exceptions, it just has a try/catch block surrounding code inside of its
for loop. Running this code with JDK 1.1.4 on an unloaded Windows NT 4.0 machine produces
results shown in Table 2. The average time indicated is based on 50 runs of the program with the
specified array size.
Table 2. Average times for the code in Listing 5.
|
Array Size
|
Average time for method1()
|
Average time formethod2()
|
10,000
|
5 ms
|
6 ms
|
100,000
|
51 ms
|
59 ms
|
1,000,000
|
507 ms
|
588 ms
|
We notice that method2() is about 16% slower than
method1() for large arrays. Because no exceptions are being thrown,
this is simply a function of method2() having a
try/catch block in its version of the for loop. If the
try/catch block is placed outside of the for loop, there is
no difference in the execution time of the two methods.
In general, it is a better programming practice to place try/catch blocks
outside of loops. Failure to do this can have a negative impact on the runtime performance of your
code. Having said this, it is true that you will not see slower results on some JVMs. Running this
code on JDK 1.2 does not show any difference in the timings. However, you may not know which JVMs
your customers will be running, so it is still a good idea not to place
try/catch blocks inside of loops.
Faster Code With Exceptions?
After what we have seen so far, you may not think it's possible to write Java code that utilizes
exceptions that is faster than Java code that does not. There are actually cases where this is
possible. Let's consider the code in
Listing 6.
Listing 6 contains two methods, method1() and method2(),
at //1 and //3, that contain almost identical
code. Method1() at //1 creates an array of
integers, then initializes each value in the array from 0 to the size
of the array minus 1. Notice at //2, the
loop to perform the assignments is a typical for loop in Java. It starts at
0, increments by 1, and terminates when the
counter becomes equal to the size of the array. The time it takes to complete this operation, in
milliseconds, is then calculated and returned.
Let's compare this to method2() at //3.
Method2() performs the exact same operation on an array, but
the for loop is different. Notice at //4 the for loop is
coded to run indefinitely. It still starts at the value 0 and
increments by 1, but it does not check when the counter becomes equal
to the size of the array. Instead, this code waits for the
ArrayIndexOutOfBoundsException to terminate the loop
at //5. The time it takes to complete this operation,
in milliseconds, is then calculated and returned.
Running this code with JDK 1.1.4 on an unloaded Windows NT 4.0 machine produces the results
shown in Table 3. The average time indicated is based on 50 runs of the program with the
specified array size.
Table 3. Average times for the code in Listing 6.
|
Array Size
|
Average time for method1()
|
Average time for method2()
|
10,000
|
5 ms
|
4 ms
|
100,000
|
50 ms
|
42 ms
|
1,000,000
|
497 ms
|
410 ms
|
Table 3 indicates that the time to execute the loop in Listing 6 at //4
is about 17% faster than the loop at //2 for large arrays. What is
going on here? We just discussed how throwing exceptions is expensive and having
try/catch blocks in your code can slow it down. Given this, how can the
code in method2() in Listing 6 take less time to execute than the code
in method1()?
The main reason is because the for loop in method1() at
//2 performs two checks each time through to see if it has reached
the end. The first check is done with the i statement of the for loop.
The second check is performed by the Java runtime to see if the array access is out of bounds or not.
The for loop in method2() at //4 eliminates
the first check and relies on the Java runtime to perform the array bounds check. The Java runtime
will throw the ArrayIndexOutOfBoundsException if you attempt to access
beyond the bounds of an array. Therefore, the loop in method1() performs
two checks: one in the loop and the other in the Java runtime. The code in
method2() only performs one check in the Java runtime because
its for loop does not check an end condition. When the loop has reached beyond the
end of the array, the Java runtime throws the ArrayIndexOutOfBoundsException.
This exception is caught at //5 and the loop is terminated. Also note
that the try/catch block is placed outside of the for loop.
This is crucial for the performance gain.
Note, regarding the technique used in method2() of Listing 6I do not
recommend using this technique casually. It can be confusing and is not a standard way to program. Using
it only when you have a very performance-critical area of code. In addition, not all JVMs exhibit the
same timing behavior. I ran the same code on JDK 1.2 and saw no difference in the timings. This is due
to the different optimizations used in JVMs. Therefore, I highly recommend before using this technique
that you profile carefully to determine that this actually results in faster code on the JVM you are
going to run on. Also, if you use this trick, comment it well for the person who may modify the code
at a later time.
EFFECTIVE EXCEPTION HANDLING
All of the above information is useful, but I don't want to lose sight of what you are trying to
accomplish when an exception is thrown. Throwing an exception is easy. The hard part is minimizing
the damage you can cause by throwing one.
Object State Management
What good is it to throw an exception if you leave an object in an invalid or undefined state? After
all, the code catching the exception you throw may handle it and recover and call your code again.
If your object was left in a bad state, there is a chance that the code will fail. This situation
can be more difficult to debug than if you just terminated the program on the first exception instead
of trying to recover from it. When throwing exceptions, it is important to consider what state your
object is currently in. If it's in an invalid state, consider what has to be done to place it in a
valid state before throwing the exception. Consider the code in
Listing 7.
Listing 7 contains an add() method, which adds an object to some list. The
first thing it does is increment a counter at //1 for the number of objects
in the list. It then conditionally reallocates the list and adds the object to the list at
//2. The code in Listing 7 is flawed. Notice that the code between
//1 and the addToList() call at
//2 can throw exceptions. If an exception is thrown after
//1, the object is now in an invalid state because the counter,
numElements, is incorrect. If the caller of this method recovered from
the thrown exception and called this method again, there is a good chance that other problems will
arise because the object state is invalid.
The fix for this problem is a simple one and is shown in
Listing 8.
We simply move the increment of numElements to the end of the method
at //1. This ensures the counter, numElements, is
accurate because we increment it after we have successfully added the object to the list. This is a
simple example, but it exposes a potentially serious problem. You need not only worry about the
object you are currently running in, but other objects you may have modified in your method.
You may have completely or partially created a file, opened a socket, or made remote method
calls to another machine when you threw an exception and exited the method. You need to be
aware of the object states that exist on your local machine, or a remote machine, if you expect
your code to work properly when reentered at a later time.
If you expect the callers of your code to recover from the exceptions you generate, you should
review all of the places where you generate exceptions to see if you are leaving your objects
in a valid state. If you are not, you need to ensure that if your code is entered again, it
will work properly. The finally keyword can and should be used to
help preserve object state.
Unfortunately, this is extremely difficult to do properly. In your method, you may have called
many other methods that have changed many states of objects. If you throw an exception and
leave your method, you will have quite a bit of work to clean up to get back to a state where
your code is valid again. This problem is not easily solved and requires much thought and
effort. It is the single biggest inhibitor in producing really robust Java code that utilizes
exceptions properly.
SUMMARY
Exception handling, although useful, is not a cure-all for your error handling problems. There are
many areas of exception handling that are not very obvious and can lead to more complexity and
more problems. Used properly and carefully, exception handling has many benefits. It has its
share of pitfalls, and these can and should be avoided through judicious use of these concepts.
Quantity reprints of this article can be purchased by phone: 717.560.2001, ext.39 or by email:
[email protected].