Section 9.3
Exceptions and the try...catch Statement
GETTING A PROGRAM TO WORK UNDER IDEAL
circumstances is usually a lot easier than making the program
robust. A robust program
can survive unusual or "exceptional" circumstances
without crashing. One approach to writing robust programs is
to anticipate the problems that might arise and to include
tests in the program for each possible problem. For example,
a program will crash if it tries
to use an array element A[i], when i is not
within the declared range of indices for the array A.
A robust program must anticipate the possibility of a bad index and
guard against it. This could be done with an if statement:
if (i < 0 || i >= A.length) {
... // Do something to handle the out-of-range index, i
}
else {
... // Process the array element, A[i]
}
There are some problems with this approach. It is difficult and
sometimes impossible to anticipate all the possible things that
might go wrong. It's not always clear what to do when an error is
detected. Furthermore, trying to anticipate all the possible
problems can turn what would otherwise be a straightforward
program into a messy tangle of if statements.
Java (like its cousin, C++) provides a neater, more structured
alternative method for dealing with errors that can occur
while a program is running. The method is referred to as
exception-handling. The word
"exception" is meant to be more general than "error."
It includes any circumstance that arises as the program is executed
which is meant to be treated as an exception to the normal flow of
control of the program. An exception might be an error, or it might
just be a special case that you would rather not have clutter up
your elegant algorithm.
When an exception occurs during the execution of a program,
we say that the exception is thrown.
When this happens, the normal flow of the program is thrown off-track,
and the program is in danger of crashing. However, the crash can
be avoided if the exception is caught
and handled in some way. An exception can be thrown in one part
of a program and caught in a different part. An exception
that is not caught will generally cause the program to crash.
(More exactly, the thread that throws the exception will crash.
In a multithreaded program, it is possible for other threads to
continue even after one crashes.)
By the way, since Java programs are executed by a Java interpreter,
having a program crash simply means that it terminates abnormally
and prematurely. It doesn't mean that the Java interpreter will
crash. In effect, the interpreter catches any exceptions that
are not caught by the program. The interpreter responds by terminating
the program. (In the case of an applet, only the current operation -- such
as the response to a button -- will be terminated. Parts of the applet
might continue to function even when other parts are non-functional
because of exceptions.) In many other programming
languages, a crashed program will often
crash the entire system and freeze the computer until it is restarted.
With Java, such system crashes should be impossible -- which means that
when they happen, you have the satisfaction of blaming the system
rather than your own program.
When an exception occurs, the thing that is actually "thrown"
is an object. This object can carry information (in its instance
variables) from the point where the exception occurs to the
point where it is caught and handled. This information always includes
the subroutine call stack, which is a list
of the subroutines that were being executed when the exception was thrown.
(Since one subroutine can call another, several subroutines can be
active at the same time.) Typically, an exception object also
includes an error message describing what happened to cause the
exception, and it can contain other data as well. The object
thrown by an exception must be an instance of the standard class java.lang.Throwable
or of one of its subclasses. In general, each different type of exception
is represented by its own subclass of Throwable. Throwable
has two direct subclasses, Error and Exception. These
two subclasses in turn have many other predefined subclasses. In addition,
a programmer can create new exception classes to represent new types
of exceptions.
Most of the subclasses of the class Error represent serious
errors within the Java virtual machine that should ordinarily cause
program termination because there is no reasonable way to handle them.
You should not try to catch and handle such errors. An example
is the ClassFormatError, which occurs when the Java
virtual machine finds some kind of illegal data in a file that
is supposed to contain a compiled Java class. If that class was
being loaded as part of the program, then there is really no way
for the program to proceed.
On the other hand, subclasses of the class
Exception represent exceptions that
are meant to be caught. In many cases, these are exceptions that
might naturally be called "errors," but they are errors
in the program or in input data that a programmer can anticipate
and possibly respond to in some reasonable way. (However, you should
avoid the temptation of saying, "Well, I'll just put a thing
here to catch all the errors that might occur, so my program won't
crash." If you don't have a reasonable way to respond to
the error, it's usually best just to terminate the program,
because trying to go on will probably only lead to worse things
down the road -- in the worst case, a program that gives an
incorrect answer without giving you any indication that the
answer might be wrong!)
The class Exception has its own subclass,
RuntimeException. This class groups together many common exceptions
such as: ArithmeticException, which occurs for example when there
is an attempt to divide an integer by zero, ArrayIndexOutOfBoundsException,
which occurs when an out-of-bounds index is used in an array, and
NullPointerException, which occurs when there is an attempt
to use a null reference in a context when an actual object
reference is required. A RuntimeException generally indicates
a bug in the program, which the programmer should fix.
RuntimeExceptions and Errors
share the property that a program can simply ignore the possibility
that they might occur. ("Ignoring" here means that you are content
to let your program crash if the exception occurs.)
For example, a program does this every time
it uses an array reference like A[i] without making arrangements to catch
a possible ArrayIndexOutOfBoundsException. For all other exception
classes besides Error, RuntimeException, and
their subclasses, exception-handling is "mandatory" in
a sense that I'll discuss below.
The following diagram is a class hierarchy showing the
class Throwable and just a few of its subclasses.
Classes that require mandatory exception-handling are shown
in red.
To catch exceptions in a Java program, you need a try
statement. The idea is that you tell the computer to "try"
to execute some commands. If it succeeds, all well and good.
But if an exception is thrown during the execution of those commands,
you can catch the exception and handle it. For example,
try {
double determinant = M[0][0]*M[1][1] - M[0][1]*M[1][0];
System.out.println("The determinant of M is " + determinant);
}
catch ( ArrayIndexOutOfBoundsException e ) {
System.out.println("M is the wrong size to have a determinant.");
}
The computer tries to execute the block of statements following
the word "try". If no exception occurs during
the execution of this block, then the "catch"
part of the statement is simply ignored. However, if an
ArrayIndexOutOfBoundsException occurs, then the computer jumps
immediately to the block of statements labeled
"catch (ArrayIndexOutOfBoundsException e)". This
block of statements is said to be an exception
handler for ArrayIndexOutOfBoundsException. By handling the
exception in this way, you prevent it from crashing the program.
You might notice that there is another possible source of
error in this try statement. If the value
of the variable M is null,
then a NullPointerException will be thrown when
the attempt is made to reference the array. In the above try
statement, NullPointerExceptions are not caught, so they
will be processed in the ordinary way (by terminating the program,
unless the exception is handled elsewhere).
You could catch NullPointerExceptions by adding another
catch clause to the try statement:
try {
double determinant = M[0][0]*M[1][1] - M[0][1]*M[1][0];
System.out.println("The determinant of M is " + determinant);
}
catch ( ArrayIndexOutOfBoundsException e ) {
System.out.println("M is the wrong size to have a determinant.");
}
catch ( NullPointerException e ) {
System.out.print("Programming error! M doesn't exist: " + );
System.out.println( e.getMessage() );
}
This example shows how to use multiple catch
clauses in one try block. It also shows what
that little "e" is doing in the catch clauses.
The e is actually a variable name. (You can use any
name you like.) Recall that when an exception occurs, it is
actually an object that is thrown. Before executing a catch
clause, the computer sets this variable to refer to the exception object
that is being caught. This object contains information about the
exception. For example, an error message describing the
exception can be retrieved using the object's getMessage()
method, as is done in the above example. Another useful method
in every exception object, e, is e.printStackTrace().
This method will print out the list of subroutines that were being
executed when the exception was thrown. This information can
help you to track down the part of your program that caused the
error.
Note that both ArrayIndexOutOfBoundsException and
NullPointerException are subclasses of RuntimeException.
It's possible to catch all RuntimeExceptions with a single
catch clause. For example:
try {
double determinant = M[0][0]*M[1][1] - M[0][1]*M[1][0];
System.out.println("The determinant of M is " + determinant);
}
catch ( RuntimeException e ) {
System.out.println("Sorry, an error has occurred.");
e.printStackTrace();
}
Since any object of type ArrayIndexOutOfBoundsException or of type
NullPointerException is also of type RuntimeException,
this will catch array index errors and null pointer errors as well as any other
type of runtime exception. This shows why exception classes are organized into
a class hierarchy. It allows you the option of casting your net narrowly to catch
only a specific type of exception. Or you can cast your net widely to catch a wide class of
exceptions.
The example I've given here is not particularly realistic.
You are not very likely to use exception-handling to guard against null pointers and
bad array indices. This is a case where careful programming
is better than exception handling: Just
be sure that your program assigns a reasonable, non-null
value to the array M. You would certainly resent it if the designers
of Java forced you to set up a try...catch statement
every time you wanted to use an array! This is why handling
of potential RuntimeExceptions is not mandatory. There
are just too many things that might go wrong! (This also shows
that exception-handling does not solve the problem of program
robustness. It just gives you a tool that will in many cases
let you approach the problem in a more organized way.)
The syntax of a try statement is a little more
complicated than I've indicated so far. The syntax can
be described as
try {
statements
}
optional-catch-clauses
optional-finally-clause
Note that this is a case where a block of statements, enclosed between
{ and }, is required. You need the { and } even if they enclose just one
statement. The
try statement can include zero or more catch
clauses and, optionally, a finally clause. (The try
statement must include either a finally clause or at
least one catch clause.) The syntax for a catch
clause is
catch ( exception-class-name variable-name ) {
statements
}
and the syntax for a finally clause is
finally {
statements
}
The semantics of the finally clause is that the block of
statements in the finally clause is guaranteed to be
executed as the last step in the execution of the try statement,
whether or not any exception occurs and whether or not any exception
that does occur is caught and handled. The finally clause
is meant for doing essential cleanup that under no circumstances
should be omitted.
There are times when it makes sense for a program to
deliberately throw an exception. This is the case when
the program discovers some sort of exceptional or error
condition, but there is no reasonable way to handle the
error at the point where the problem is discovered. The
program can throw an exception in the hope that some
other part of the program will catch and handle the exception.
To throw an exception, use a throw statement.
The syntax of the throw statement is
throw exception-object ;
The exception-object must
be an object belonging to one of the subclasses of Throwable.
Usually, it will in fact belong to one of the subclasses of
Exception. In most cases, it will be a newly constructed
object created with the new operator. For example:
throw new ArithmeticException("Division by zero");
The parameter in the constructor becomes the error message in the
exception object. (You might find this example a bit odd, because
you might expect the system itself to throw an ArithmeticException
when an attempt is made to divide by zero. So why should a programmer
bother to throw the exception? The answer is a little
surprising: If the numbers that are being divided are of type
int, then division by zero will indeed throw an
ArithmeticException. However, no arithmetic operations
with floating-point numbers will ever produce an exception.
Instead, the special value Double.NaN is used to represent
the result of an illegal operation.)
An exception can be thrown either by the system or by a throw
statement. The exception is processed in exactly the same way in either case.
Suppose that the exception is thrown inside a try statement.
If that try statement has a catch clause that handles
that type of exception, then the computer jumps to the catch
clause and executes it. The exception has been handled.
After handling the exception, the computer executes the finally clause
of the try statement, if there is one. It then continues normally with
the rest of the program which follows the try statement.
If the exception is not immediately caught and handled, the processing of
the exception will continue.
When an exception is thrown during the execution of a subroutine
and the exception is not handled in the same subroutine, then
that subroutine is terminated (after the execution of any pending
finally clauses). Then the routine that called that
subroutine gets a chance to handle the exception. That is, if the subroutine
was called inside a try statement that has an appropriate
catch clause, then that catch clause will be executed
and the program will continue on normally from there.
Again, if that routine does not handle the exception,
then it also is terminated and the routine that called
it gets the next shot at the exception. The exception
will crash the program only if it passes up through the entire
chain of subroutine calls without being handled.
A subroutine that might generate an exception can announce this
fact by adding the clause "throws
exception-class-name"
to the header of the routine. For example:
static double root(double A, double B, double C)
throws IllegalArgumentException {
// Returns the larger of the two roots of
// the quadratic equation A*x*x + B*x + C = 0.
// (Throws an exception if A == 0 or B*B-4*A*C < 0.)
if (A == 0) {
throw new IllegalArgumentException("A can't be zero.");
}
else {
double disc = B*B - 4*A*C;
if (disc < 0)
throw new IllegalArgumentException("Discriminant < zero.");
return (-B + Math.sqrt(disc)) / (2*A);
}
}
As discussed in the previous section,
The computation in this subroutine has the preconditions that
A != 0 and B*B-4*A*C >= 0.
The subroutine throws an exception of type IllegalArgumentException
when either of these preconditions is violated. When an illegal condition is found
in a subroutine, throwing an exception is often a reasonable response. If the
program that called the subroutine knows some good way to handle the error,
it can catch the exception. If not, the program will crash -- and the programmer
will know that the program needs to be fixed.
Mandatory Exception Handling
In the preceding example, declaring that the subroutine root() can throw an
IllegalArgumentException is just a courtesy to potential readers
of this routine. This is because handling of IllegalArgumentExceptions
is not "mandatory". A routine can throw an IllegalArgumentException
without announcing the possibility. And a program that calls that routine
is free either to catch or to ignore the exception, just as a programmer
can choose either to catch or to ignore an exception of
type NullPointerException.
For those exception classes that require mandatory handling,
the situation is different. If a subroutine can throw such an
exception, that fact must be announced in a throws
clause in the routine definition. Failing to do so is a syntax
error that will be reported by the compiler.
On the other hand, suppose that some statement in a program
can generate an exception that
requires mandatory handling. The statement could be a
throw statement, which throws the exception directly,
or it could be a call to a subroutine that can throw the exception.
In either case, the exception must be handled. This can be done in
one of two ways: The first way is to place the statement
in a try statement that has a catch clause
that handles the exception. The second way is to declare
that the subroutine that contains the statement can throw the exception.
This is done by adding a "throws" clause to the subroutine heading.
If the throws clause is used,
then any other routine that calls the subroutine will be responsible for
handling the exception. If you don't
handle the possible exception in one of these two ways, it will be considered
a syntax error, and the compiler will not accept your program.
Exception-handling is mandatory for any exception
class that is not a subclass of either Error or
RuntimeException. Exceptions that require mandatory
handling generally represent conditions that are outside
the control of the programmer. For example, they might represent
bad input or an illegal action taken by the user. A robust program has
to be prepared to handle such conditions. The design of Java
makes it impossible for programmers to ignore such conditions.
Among the exceptions that require mandatory handling are several
that can occur when using Java's input/output routines. This means
that you can't even use these routines unless you understand
something about exception-handling. The next chapter
deals with input/output and uses exception-handling extensively.