Exceptional coding
- By Ethan Henry
- March 24, 2001
IN MY LAST "Java In Practice"
1 column, I talked about what a valuable tool a structured logging framework can be in your applications. After that, I received some feedback from some readers who were kind enough to point out a couple of great logging toolkits. The first is the logging toolkit for Java at IBM's alphaWorks site and the second is The Jakarta Log4J Package. Both are fully featured logging toolkits, and I've been told that both toolkits are looking at compatibility with the proposed standard logging API that's being defined by Java Specification Request (JSR-47).
This month, I discuss another element in cre-ating robust Java applications—the proper use of exceptions. Exceptions are a mechanism for in-dicating when something has happened in your program that is unusual, unexpected, well ... exceptional. Exceptions provide one of the best (and in some cases, the only) mechanisms for writing robust, resilient code that's capable of dealing with the unexpected. Unfortunately, far too many developers seem to treat exceptions like an annoyance and don't give much thought to handling them.
First, let's have a brief refresher on how Java's exception mechanism works. Exceptions are represented by an object that is an instance of some subclass of java.lang.Throwable. The two primary subclasses of Throwable are java.lang.Error and java.lang.Exception. Error objects represent some sort of horrible error that comes from either a corrupt class file or an error in the JVM itself. We won't worry about errors too much, as these typically represent a condition too complex for the program to self-correct. Exceptions are further subdivided into two types: unchecked and checked exceptions. Unchecked exceptions are the ones that the compiler does little to enforce the correct usage of. NullPointerException is a good example of this—it would be more than slightly onerous to require developers to enclose all dereferencing operations (via the "." operator) in try/catch blocks. Unchecked exceptions don't derive from Exception directly, but are instead represented by subclasses of java.lang.RuntimeException, which derives from Exception. Finally, checked exceptions are exceptions that the compiler enforces some standards on—checked exceptions can only be thrown from methods that include that type of exception in the throws clause of the method declaration. Checked exceptions have to be handled by either passing them on or by dealing with them in a try/catch block.
Exceptions are generated one of two ways: They're either generated by the VM, when you dereference a null pointer for example, or by creating an instance of some Exception subclass and "throwing" it with the throw keyword. Once an exception has been generated, the VM "unwinds the stack"—it backtracks up the chain of method calls looking for a try block. Once it finds a try block, it looks for a catch block whose parameter type matches the type of exception that's been caught. If there's no appropriate catch block, the VM keeps unwinding the stack. If it gets back to the top of the current thread's stack without handling the exception, the VM finds the ThreadGroup for the current thread and calls the ThreadGroup's uncaughtException method. The default implementation of ThreadGroup.uncaughtException simply takes the exception and prints a stack trace describing it to System.err.
The final element of the exception handling is the finally keyword. A "finally block" can be associated with each try block. This block of code is executed after the completion of the try/catch block, regardless of how the flow of execution leaves. For example, consider the following short piece of code:
public class Excep {
public static void main(String args[]) {
int i, j;
for(i=-5;i<=5;i++) {="" try="" {="" j="10/i;" if(j%2="=" 0)="" continue;="" }="" catch(runtimeexception="" e)="" {="" system.out.println("wow,="" an="" exception");="" }="" finally="" {="" system.out.println("this="" always="" gets="" printed");="" }="" system.out.println(i);="" }="" }="" }="">=5;i++)>
In this example, the finally block is executed every iteration through the loop (11 times in total), while the code after the try/catch/finally block isn't always executed. Note the continue statement could be replaced by a break, return, or throw statement and the finally block would still get executed, regardless of where the flow of execution goes afterwards.
2
The reason I go into such detail about exceptions is:
- How much of the extremely abbreviated description were you already familiar with?
- How much more do you know about exceptions aside from what I've described?
- Do you know what happens with object locks when an exception is thrown from inside a synchronized method?
- Did you know that you can replace the default unhandled exception behavior by creating your own ThreadGroup subclass and overriding uncaughtException?
Maybe you did. Good for you. But if you don't know everything there is to know about exceptions—and I'm serious here—why not? Sure, it sounds ludicrous to say that you have to know
everything about exceptions, but there really isn't a lot to know. Knowing how to properly use exceptions lets you build applications that can deal with both expected and unexpected situations in a robust manner, which is something that every developer wants to do.
So, what kind of things should you do to properly deal with exceptions?
First of all, do some analysis on your application. Where are exceptions likely to occur? Which exceptions are going to cause serious problems and which are un- important? For example, getting an IOException while trying to read in some icons for your GUI is probably not very serious (though the user should still ideally have a way of accomplishing the same operation without being able to see the icons). Being unable to look up an EJB via the Java Naming and Directory Interface (JNDI), however, is probably pretty important. Figuring out where and how to handle exceptions is somewhat like performance tuning in this respect—there's no point in tuning parts of your application that are rarely ever run or aren't part of the application's core logic. But you should pay careful attention to the handling of frequently executed exceptions in the code.
Don't ignore checked exceptions. The most frequently encountered types of checked exceptions are java.io.IOException and its various subclasses, such as java.rmi.RemoteException. It can be irritating sometimes when you're just trying to make a simple I/O call to read in some data, but since almost every I/O-related method throws an IOException, almost every I/O call has to be inside a try block. Those exceptions can tell you some pretty useful information though, such as abnormal termination of input from a file or socket. If a file mysteriously disappears during an operation (deleted by an unknowing user, say), most Java apps will do nothing more than simply print an exception stack trace and chug along with whatever data it has managed to read into memory. It doesn't take much effort to do something a bit more useful, such as sending a more descriptive message to the user or to a logging system. Better yet, why not re-attempt the I/O operation—perhaps the problem was transient, or perhaps something has really changed and no processing should be done on the data.
Another good practice is to avoid leaving an object in a bad state if an exception occurs. This means you should try to put all the code that changes data members at the end of a method so that it's executed together, or if you catch an exception during a complex operation, undo any changes you've made to the object. Database programmers have been doing this for years—the idea that you should always leave data in a valid state is the basis of two-phase commit and transactional processing systems. Automatic state maintenance is one of the appeals of using EJBs and the Java Transaction API. But if you're not developing a J2EE-based application, you'll have to handle this sort of thing manually. One technique for doing this is to use a finally block in conjunction with any code that allocates external resources (such as files or sockets) or changes an object's state to clean everything up.
Avoid using "catch-all" blocks that catch all exceptions via something like catch(Exception e) { ... }. The problem with this is that if an exception occurs that you weren't expecting, you may not even realize it. Be as specific as possible when creating catch blocks so that other types of exceptions can be passed on to a try/catch block higher up the stack or to ThreadGroup.uncaughtException. Handling too many exceptions at any given point in a program denies other parts of the application the chance to potentially do something more useful with the information.
Exceptions should be used solely for the purpose of error handling and recovery and shouldn't be used for normal program flow control. This is because there is a performance penalty associated with throwing exceptions and searching up the stack for catch blocks. Note that during normal program execution where no exceptions are thrown there is no performance penalty for using try/catch blocks. But using throw as a replacement for return is a bad idea.
Finally, take into consideration what kind of program you're writing. Should a J2ME application running on a PDA behave the same way when it encounters an exception as a desktop-based J2SE app? Displaying an exception stack trace is probably not what someone using a Java-enabled cell phone wants to see. Maybe you should just restart the application and ensure no data corruption has occurred. What about J2EE applications? In these cases, you'll probably want to save the exception's stack trace somewhere, but dumping it out to System.err is probably not the best thing to do. Write the exception to your log and try the operation again. If it fails a second time, flag a high-priority error (your logging mechanism lets you specify priorities on messages, right?), send an e-mail to the system administrator, and perhaps try to obtain the information in a different way. Finally, J2SE applications may be able to automatically connect across the Internet to send diagnostic information about application failures back to the developer.
Exceptions, for all the power they offer, are frequently misunderstood and underutilized by developers. How can I say that? I've spoken at and attended a number of conferences where sessions on the basics of exception handling were the most popular sessions at the conference. Admittedly, conferences are biased toward newer Java developers, but still, I believe most developers think exceptions are too hard to use, too hard to understand, or just too much work to use in their applications. And nothing could be further from the truth—why, I bet I could explain it all to you in one short column.
References
- Henry, E. "The value of logging," Java Report, Vol. 5, No. 10, Oct. 2000, pp. 82–84.
- Gosling, J., B. Joy, and G. Steele. The Java Language Specification. Addison–Wesley, 1996, http://java.sun.com/docs/books/jls/second_edition/html/exceptions.doc.html.