Trail: Essential Java Classes
|
Lesson: Handling Errors with Exceptions
|
|
What's an Exception and Why Do I Care?
The term exception is shorthand for the phrase "exceptional event".
It can be defined as follows:
Definition: An exception is an event that occurs
during the execution of a program that disrupts the normal flow of instructions.
Many kinds of errors can cause exceptions--problems ranging from
serious hardware errors, such as a hard disk crash, to simple programming errors,
such as trying to access an out-of-bounds array element.
When such an error occurs within a Java method, the method creates an
exception object and hands it off to the runtime system. The exception object
contains information about the exception, including its type and the state of
the program when the error occurred. The runtime system is then responsible for
finding some code to handle the error. In Java terminology, creating an exception
object and handing it to the runtime system is called throwing an exception.
After a method throws an exception, the runtime system leaps into action to
find someone to handle the exception. The set of possible "someones" to
handle the exception is the set of methods in the call stack of the method
where the error occurred. The runtime system searches backwards through the
call stack, beginning with the method in which the error occurred, until
it finds a method that contains an appropriate exception handler. An
exception handler is considered appropriate if the type of the exception thrown
is the same as the type of exception handled by the handler. Thus the exception
bubbles up through the call stack until an appropriate handler is found
and one of the calling methods handles the exception. The exception handler chosen
is said to catch the exception.
If the runtime system exhaustively searches all of the methods on the call
stack without finding an appropriate exception handler, the runtime system
(and consequently the Java program) terminates.
By using exceptions to manage errors, Java programs have the following advantages
over traditional error management techniques:
Advantage 1: Separating Error Handling Code from "Regular" Code
In traditional programming, error detection, reporting, and handling often lead
to confusing spaghetti code. For example, suppose that you have a function that
reads an entire file into memory. In pseudo-code, your function might look something
like this:
readFile {
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
}
At first glance this function seems simple enough, but it ignores all of these
potential errors:
-
What happens if the file can't be opened?
-
What happens if the length of the file can't be determined?
-
What happens if enough memory can't be allocated?
-
What happens if the read fails?
-
What happens if the file can't be closed?
To answer these questions within your read_file function,
you'd have to add a lot of code to do error detection, reporting and handling.
Your function would end up looking something like this:
errorCodeType readFile {
initialize errorCode = 0;
open the file;
if (theFileIsOpen) {
determine the length of the file;
if (gotTheFileLength) {
allocate that much memory;
if (gotEnoughMemory) {
read the file into memory;
if (readFailed) {
errorCode = -1;
}
} else {
errorCode = -2;
}
} else {
errorCode = -3;
}
close the file;
if (theFileDidntClose && errorCode == 0) {
errorCode = -4;
} else {
errorCode = errorCode and -4;
}
} else {
errorCode = -5;
}
return errorCode;
}
With error detection built in, your original 7 lines (in bold) have been
inflated to 29 lines of code--a bloat factor of almost 400 percent. Worse,
there's so much error detection, reporting, and returning that the original
7 lines of code are lost in the clutter. And worse yet, the logical flow
of the code has also been lost in the clutter, making it difficult to tell
if the code is doing the right thing: Is the file really being closed
if the function fails to allocate enough memory? It's even more difficult
to ensure that the code continues to do the right thing after you modify
the function three months after writing it. Many programmers "solve" this
problem by simply ignoring it--errors are "reported" when their programs crash.
Java provides an elegant solution to the problem of error management:
exceptions. Exceptions enable you to write the main flow of your code and deal
with the, well, exceptional cases elsewhere. If your read_file
function used exceptions instead of traditional error management techniques,
it would look something like this:
readFile {
try {
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
} catch (fileOpenFailed) {
doSomething;
} catch (sizeDeterminationFailed) {
doSomething;
} catch (memoryAllocationFailed) {
doSomething;
} catch (readFailed) {
doSomething;
} catch (fileCloseFailed) {
doSomething;
}
}
Note that exceptions don't spare you the effort of doing the work
of detecting, reporting, and handling errors. What exceptions do provide
for you is the means to separate all the grungy details of what to
do when something out-of-the-ordinary happens from the main logic
of your program.
In addition, the bloat factor for error management code in this program
is about 250 percent--compared to 400 percent in the previous example.
Advantage 2: Propagating Errors Up the Call Stack
A second advantage of exceptions is the ability to propagate error
reporting up the call stack of methods. Suppose that the readFile
method is the fourth method in a series of nested method calls made by your
main program: method1 calls method2, which calls
method3, which finally calls readFile.
method1 {
call method2;
}
method2 {
call method3;
}
method3 {
call readFile;
}
Suppose also that method1 is the only method interested
in the errors that occur within readFile. Traditional
error notification techniques force method2 and method3
to propagate the error codes returned by readFile up the
call stack until the error codes finally reach method1--the
only method that is interested in them.
method1 {
errorCodeType error;
error = call method2;
if (error)
doErrorProcessing;
else
proceed;
}
errorCodeType method2 {
errorCodeType error;
error = call method3;
if (error)
return error;
else
proceed;
}
errorCodeType method3 {
errorCodeType error;
error = call readFile;
if (error)
return error;
else
proceed;
}
As you learned earlier, the Java runtime system searches backwards through
the call stack to find any methods that are interested in handling a particular
exception. A Java method can "duck" any exceptions thrown within it, thereby
allowing a method further up the call stack to catch it.
Thus only the methods that care about errors have to worry about detecting
errors.
method1 {
try {
call method2;
} catch (exception) {
doErrorProcessing;
}
}
method2 throws exception {
call method3;
}
method3 throws exception {
call readFile;
}
However, as you can see from the pseudo-code, ducking an exception does require
some effort on the part of the "middleman" methods. Any checked exceptions
that can be thrown within a method are part of that method's public programming
interface and must be specified in the throws clause of the method.
Thus a method informs its callers about the exceptions that it can throw, so that
the callers can intelligently and consciously decide what to do about those exceptions.
Note again the difference in the bloat factor and code obfuscation factor of
these two error management techniques. The code that uses exceptions is
more compact and easier to understand.
Advantage 3: Grouping Error Types and Error Differentiation
Often exceptions fall into categories or groups.
For example, you could imagine a group of exceptions, each of which represents
a specific type of error that can occur when manipulating an array:
the index is out of range for the size of the array, the element being
inserted into the array is of the wrong type, or the element being
searched for is not in the array.
Furthermore, you can imagine that some methods would like to handle all
exceptions that fall within a category (all array exceptions),
and other methods would like to handle specific exceptions (just the
invalid index exceptions, please).
Because all exceptions that are thrown within a Java program are first-class
objects, grouping or categorization of exceptions is a natural outcome of
the class hierarchy.
Java exceptions must be instances of
Throwable or any Throwable descendant.
As for other Java classes, you can create subclasses of the
Throwable class and subclasses of your subclasses.
Each "leaf" class (a class with no subclasses) represents a specific type of
exception and each "node" class (a class with one or more subclasses) represents
a group of related exceptions.
For example, in the following diagram, ArrayException is a subclass of
Exception (a subclass of Throwable) and has three subclasses.
InvalidIndexException, ElementTypeException, and NoSuchElementException are all
leaf classes. Each one represents a specific type of error that can
occur when manipulating an array. One way a method can exceptions
is to catch only those that are instances of a leaf class.
For example, an exception handler that handles only
invalid index exceptions has a catch statement like this:
catch (InvalidIndexException e) {
. . .
}
ArrayException is a node class and represents any error that can occur
when manipulating an array object, including those errors specifically represented
by one of its subclasses.
A method can catch an exception based on its group or general type by specifying
any of the exception's superclasses in the catch statement.
For example, to catch all array exceptions regardless of their specific type,
an exception handler would specify an ArrayException argument:
catch (ArrayException e) {
. . .
}
This handler would catch all array exceptions including InvalidIndexException,
ElementTypeException, and NoSuchElementException. You can find out precisely which
type of exception occurred by querying the exception handler parameter e.
You could even set up an exception handler that handles any Exception with this handler:
catch (Exception e) {
. . .
}
Exception handlers that are too general, such as the one shown here, can
make your code more error prone by catching and handling exceptions
that you didn't anticipate and therefore are not correctly handled within
the handler. We don't recommend writing general exception handlers as a rule.
As you've seen, you can create groups of exceptions and handle exceptions in a general
fashion, or you can use the specific exception type to differentiate exceptions and
handle exceptions in an exact fashion.
What's Next?
Now that you understand what exceptions are and the advantages of using
exceptions in your Java programs, it's time to learn how to use them.
|