import java.*: Understanding Java Exceptions
Chuck Allison
Compared to C++, Java is fairly strict when it comes to exceptions. You may even have to prove to the compiler that you know how to handle them.
Although Ive mentioned exceptions in this column before, its time to take a deeper look. You just cant be effective as a Java developer without understanding why exceptions are part of the language, and how to use them effectively.
Out of Obscurity
In traditional programming languages like C, you usually use return values from a function to indicate an error condition. This practice unfortunately requires you to generate and check return values through every function in a nested chain of function calls, filling your source code with clutter that obscures the logic of your algorithms. Using exceptions, on the other hand, lets you handle errors and still have clear, readable code. Suppose, for example, that in main I call a function f, which calls a function g, which finally calls a function h. Without exceptions (or some similar mechanism [1]), if something goes wrong in h, you have to pass a return code back to g, then back to f, and finally back to main. With exceptions, all you have to do is put the call to f in a try block and make provisions for handling the error immediately thereafter, in a catch block such as in the following code snippet.
// In main... try { f(); } catch (MyException e) { System.out.println(e); }
All you have to do to raise an exception in h is to throw it, like this:
// In h... throw new MyException("optional text here");
The functions f and g dont need to act as middlemen to propagate error-handling information up the call stack, as they would in C. Thats what I mean by minimizing code clutter.
Source code clutter of course doesnt affect an end user, who couldnt care less whether you write pretty code or not. He who ultimately pays your salary wants software that works and that doesnt go ballistic should something unusual occur at run time. Here again, exceptions save the day because exceptions dont like to be ignored. Java is so fastidious about how you use exceptions, in fact, that you cant even compile a program successfully without proving that you know how to handle most of the exceptions that may raise their ugly heads when your program runs. But before we look at how to handle exceptions, lets talk about where they come from.
Whence Exceptions?
One source of exceptions, of course, is when a throw statement executes; either in your own code or in code you call from other classes. A method should throw an exception whenever it is unable to fulfill its specification, not as an alternate way of returning normally from a function. The method Arrays.binarySearch, for example, returns the zero-based index of the location where it finds the value youve requested. If your search value is not in the array, it returns a very useful negative number (the opposite of where it should be plus one). It would be foolish to throw an exception in this case, because not finding a value in an array is normal behavior, and the return value is well defined. Exceptions are for the truly exceptional things that arent normally supposed to happen.
As an example of when you should throw an exception, consider a stack data structure. The class in Listing 1 implements a fixed-size stack; you pass the stacks capacity in a constructor argument. The contract of the pop method requires that the stack not be empty when it is called, so if you try to pop something from an empty stack, thats exceptional. There is no meaningful value to return in this case, and you just plain shouldnt have made the call in the first place. Similar logic applies to the top and push methods.
The only objects that can be thrown or caught by Javas exception handling mechanism are instances of classes in the Throwable class hierarchy (see Figure 1). Most exceptions that application programmers create extend a class in the Exception sub-hierarchy (most often Exception itself), as StackException does in Listing 2. By convention, all exception classes should have two constructors: a no-arg constructor and one that takes a single String argument, which allows you to include useful additional information at the throw point.
Classes that extend Exception are known as checked exceptions because the compiler checks to see whether two things occur in a program using these classes:
- Every method that throws a checked exception must advertise it in the throws clause in its method definition, and
- Every method that calls a method that advertises a checked exception must either handle that exception (with try and catch) or must in turn advertise that exception in its own throws clause.
Because StackException is a checked exception I must include it in the throws clause of pop, top, and push. Furthermore, I need to convince the compiler that I know about these exceptions when I call these methods, as I did in Listing 3. In StackTest.doTest, each call to one of those three FixedStack methods is in a try block that can catch a StackException object. In the first try block, I force an exception by trying to add one too many objects to the stack. In the second try block, even though Ive rigged things so that no StackException can occur, I still need the try and catch to make the compiler happy because every checked exception must either be handled or passed up the line. Im so confident that nothing can go wrong here that Ill throw an InternalError, which aborts the program with an error message (more on this later).
Unchecked Exceptions
Just as checked exceptions are useful for signaling when your methods cannot fulfill their contract, there are other potential errors outside of your control, such as memory exhaustion, that can prevent the Java virtual machine from fulfilling its specification. Since you cant plan for such errors ahead of time, you would have to catch them everywhere, which defeats the principle of maintaining uncluttered code. Therefore, these errors are unchecked exceptions, meaning exceptions that you dont have to include in a throws clause. You are welcome to catch them (well, some of them), but the compiler wont make you do it.
Unchecked exceptions fall into two categories: those that extend RuntimeException [2], and those that extend Error. I realize that I said earlier that classes inheriting from class Exception are checked exceptions, but thats only half true: the whole truth is that classes in the Exception hierarchy other than those in the RuntimeException sub-hierarchy are checked exceptions.
Exceptions that extend RuntimeException represent errors that you may want to handle, although youre not required to. The most common ones include:
- ArithmeticException when a divide by zero is attempted
- ArrayStoreException when you try to store an object in an array of incompatible type (usually occurs when using arrays of Object, since the compiler will catch any explicit type error)
- IndexOutOfBoundsException when an array index is out of range, for example
- NegativeArraySizeException obvious
- NullPointerException when you try to dereference a null object reference
As I stated above, RuntimeExceptions are not checked because making you advertise them would have no effect on establishing the correctness of your methods and would unnecessarily clutter your otherwise very readable code. Exceptions derived from the Error class, on the other hand, are unchecked because you never want to catch them! Error exceptions are severe errors that require shutting down the virtual machine. InternalError, which I used above, extends VirtualMachineError, which is an Error subclass. OutOfMemoryError is another obvious severe error, but there are others, like StackOverflowError, and various LinkageErrors. A linkage error means something has gone amiss when the class loader tried to load a class for execution, and commonly occurs either because some external source has introduced malicious code in an attempt to circumvent Javas security mechanism, or the code came from an out-of-spec byte code generator.
Exceptions and Inheritance
When an exception occurs, the runtime mechanism searches back up the call stack for a catch block that matches the type of the exception. A match means any type for which the instanceOf operator applied to the exception returns true. In other words, catch (C c) will catch any exception that is of type C or a subclass of C. For this reason, if you want to catch all checked exceptions (not always an outlandish idea, really), you can simply catch an instance of Exception:
try { ... whatever } catch (Exception e) { // Catch all unchecked exceptions System.out.println(e); }
When looking for a match, your catch blocks are inspected in the order in which they appear in the code, so be careful. If you try to place a handler for a superclass before a subclass handler; the compiler will catch your error, since the second block is unreachable.
Everyone knows that when you define application subclasses you need to be careful not break the contract of superclass methods by overriding them incorrectly in subclasses. Exceptions are part of a methods contract, so always remember this simple rule:
- Never add exception types to the throws clause of a method overridden in a subclass
This would break the contract of the superclass method and destroy any hope of substituting a subclass instance for a superclass instance (which, as the whole world knows by now, is the whole point of object-oriented programming). You can throw an instance of a subclass of the exception type however. For example, in the following, since AnotherException extends MyException, the contract is not violated.
class MyException extends Exception {} class AnotherException extends MyException {} class Parent { public void f() throws MyException {} } class Child extends Parent { public void f() throws AnotherException {} } class OverrideTest { public static void main(String[] args) throws AnotherException { Child c = new Child(); c.f(); } }
You can also relax the specification by omitting an exception specification in the subclass method, as the following code illustrates.
class Parent { public void f() throws Exception {} } class Child extends Parent { public void f() // OK! {} } class Override { public static void main(String[] args) { Child c = new Child(); c.f(); } }
Since I didnt add anything to the specification of Child.f, an instance of Child can still stand in for a Parent instance, and substitutability is preserved. From client codes perspective, there is no harm putting a call to Child.f in a try block, even though an exception cant occur.
All of the foregoing applies to interfaces as well.
Cleaning up after Yourself
Managing resources, such as files and network connections, requires a bit of care when exceptions can occur. Consider the following program that opens a file and passes the resulting FileReader to another method for processing.
import java.io.*; class ProcessFile { public static void main(String[] args) { if (args.length > 0) { FileReader f = null; try { // Open a file: f = new FileReader(args[0]); SomeOtherClass.process(f); f.close(); } catch (IOException x) { System.out.println(x); if (f != null) f.close(); } } } }
If the FileReader constructor fails to open the file, then the IOException is caught, but f remains null, so no close attempt occurs. If the static method SomeOtherClass.process throws an IOException, then the exception is caught and f is closed in the exception handler. If no errors occur, then f is closed in the try block. There seems to be no way to avoid repeating the call to FileReader.close in the code, since we have to cover all possible paths. Since reallocating resources is something that must be done whether errors occur or not, there is a better way to do the job: the finally block. Code in a finally block executes no matter what. Using finally, only one call to close is needed:
import java.io.*; class ProcessFile { public static void main(String[] args) { if (args.length > 0) { FileReader f = null; try { // Open a file: f = new FileReader(args[0]); SomeOtherClass.process(f); } catch (IOException x) { System.out.println(x); } finally { if (f != null) f.close(); } } } }
The only thing that can prevent a finally block from executing is a call to System.exit, which shuts down the virtual machine. The general syntax for using exceptions, then, is to have a try block followed by zero or more catch blocks followed an optional finally block, but a try block cannot stand alone it must have either at least one catch block, or a finally block, or both. The program in Listing 4 shows how tenacious finally is. When executed as-is, the output is
finally! last statement
If you uncomment the line labeled with the numeral 1 in its comment, then a return from f occurs, but the finally block is still executed first, as evidenced by the output:
finally!
Now comment out line 1 again and uncomment line 2. There is no output this time since System.exit shuts downs the program entirely. With only line 3a uncommented, the exception thrown in the try block is caught in the adjacent catch, but it does nothing, so execution resumes after the catch, giving the same output as in the first instance:
finally! last statement
Now for the hard question. What happens when both 3a and 3b are uncommented? You might think that the finally block executes, and then the exception thrown in the catch in f is caught by the catch in main. The compiler is smarter than that, though. It detects that the statement in f that prints "last statement" is unreachable and gives you a compile error. Gotcha!
The program in Listing 5 attempts to open a file for reading. Notice that the function f doesnt handle any exceptions (hence its throws clause). It wouldnt know what to do with an exception if it caught one. All f wants to do is make sure that the file is closed. By having a finally without a try, f can close the file and then let the exception pass up the stack to main. If you try to execute this program without a filename on the command line, the following is printed from main:
java.lang.ArrayIndexOutOfBoundsException: 0
since args is empty. If you give the name of a file that doesnt exist, then you get this message instead:
java.io.FileNotFoundException: <file name>
If the file does exist, then you get the expected output:
file opened file closed
It is conceivable that a catch block can partially handle an exception and yet still want to pass it up the line to an enclosing try block. In this case, all you need to do is re-throw the exception at the end of the catch block (e.g., throw x;).
Note the comment near the end of f thats says // beware lost exception!!!. Since FileReader.close can also throw an exception, what do you think happens if it does? If it occurs in the midst of another exception, it becomes the current exception, so the previous exception is lost. Not much you can do about that.
Exceptions and Threads
If youve been in an unusually astute mood while reading this article, you may have noticed that thus far Ive avoided a very important issue: what happens if an exception is not caught? The key to understanding the answer to that question is to pay particular attention to the execution path that the exception runtime mechanism takes. We say that it goes back up the call stack. The key is the word stack. Where does a stack reside (I dont mean physically; I mean conceptually, as far as the JVM is concerned)? When the JVM is launched, it starts a thread for main and things progress from there. If any other threads start, they get their own stack. In other words, a stack belongs to a thread, and therefore an exception belongs to a thread. If the exception mechanism finds no matching handler, it falls off the end of its stack, and therefore the thread for that stack must terminate. So the short answer to the original question is: Its thread dies.
That sounds pretty serious, and it is, but what can we do if it happens? The thread is lost forever, but all threads belong to a thread group. Thread groups are collections of threads and can contain other thread groups. When a thread dies because of an uncaught exception, the runtime system calls the uncaughtException method in the dying threads thread group. The default behavior of ThreadGroup.uncaughtException is to call its parent thread groups uncaughtExeption method. If a thread group has no parent, then it calls Throwable.printStackTrace for the dying exception, so at least some information is printed to System.err. This is all illustrated in Listing 6. ThreadTest.main launches an instance of MyThread, which prints a message and immediately throws an exception. There is no way that this exception can be caught in main, since it comes from a different thread, so the thread dies. Both the main thread and the instance of MyThread belong to the default thread group (since we didnt cause it be otherwise), which has no parent, so the stack trace prints, but all the while the main thread is still running. I had main sleep so it could print its final message after the MyThread instance dies. If you want, you can override ThreadGroup.uncaughtException to do whatever you want, but Ill leave that explanation for an article on threads.
Summary
- Exceptions help separate error handling code from normal method logic, resulting in cleaner code.
- Exceptions will not be ignored.
- You should only throw an exception when a method cant fulfill its specification.
- All exception classes descend from java.lang.Throwable.
- You should only throw checked exceptions (i.e., descended from java.lang.Exception).
- Checked exceptions must either be handled or passed up the line by advertising them in a methods throws clause.
- The JVM will throw unchecked exceptions (i.e., descended from java.lang.RuntimeException or java.lang.Error) when it cant fulfill its specification.
- Dont catch objects descended from java.lang.Error.
- Dont break a superclass methods contract by adding to the throws clause in a subclass override.
- Use finally to manage resources.
- Beware of uncaught exceptions and be aware of ThreadGroup.uncaughtException.
Notes
[1] C offers the setjmp/longjmp mechanism, which allows you to jump from a deeply nested function to a point higher up in the execution stack without passing return codes up the stack manually. While this is fine for C, it is a big problem in object-oriented languages like C++ and Java because cleanup code that would execute under normal execution is skipped when you jump. Exceptions in both C++ and Java solve this problem. If, however, you just cant ignore your curiosity to learn about setjmp and longjmp, see my article Error Handling with C++ Exceptions, Part 1 in the November 1997 issue of CUJ.
[2] It is universally acknowledged that RuntimeException is a stupid name, since all exceptions are processed at run time. I havent heard a better alternative, though. In any case, its way too late now.
Chuck Allison is a long-time columnist with CUJ. During the day he does Internet-related development in Java and C++ as a Software Engineering Senior in the Custom Development Department at Novell, Inc. in Provo, Utah. He was a contributing member of the C++ standards committee for most of the 1990s and authored the book C & C++ Codes Capsules: A Guide for Practitioners (Prentice-Hall, 1998). He has taught mathematics and computer science at six western colleges and universities and at many corporations throughout the U.S. You can email Chuck at [email protected].