Catching
an exception
If
a method throws an exception, it must assume that exception is caught and dealt
with. One of the advantages of Java exception handling is that it allows you to
concentrate on the problem you’re trying to solve in one place, and then
deal with the errors from that code in another place.
To
see how an exception is caught, you must first understand the concept of a guarded
region
,
which is a section of code that might produce exceptions, and is followed by
the code to handle those exceptions.
The
try block
If
you’re inside a method and you throw an exception (or another method you
call within this method throws an exception), that method will exit in the
process of throwing. If you don’t want a
throw
to
leave a method, you can set up a special block within that method to capture
the exception. This is called the
try
block
because you “try” your various method calls there. The try block is
an ordinary scope, preceded by the keyword
try:
try {
// Code that might generate exceptions
}
If
you were checking for errors carefully in a programming language that
didn’t support exception handling, you’d have to surround every
method call with setup and error testing code, even if you call the same method
several times. With exception handling, you put everything in a try block and
capture all the exceptions in one place. This means your code is a lot easier
to write and easier to read because the goal of the code is not confused with
the error checking.
Exception
handlers
Of
course, the thrown exception must end up someplace. This “place” is
the
exception
handler,
and there’s one for every exception type you want to catch. Exception
handlers immediately follow the try block and are denoted by the keyword
catch:
try {
// Code that might generate exceptions
} catch(Type1 id1) {
// Handle exceptions of Type1
} catch(Type2 id2) {
// Handle exceptions of Type2
} catch(Type3 id3) {
// Handle exceptions of Type3
}
// etc...
Each
catch clause (exception handler) is like a little method that takes one and
only one argument of a particular type. The identifier (
id1,
id2,
and so on) can be used inside the handler, just like a method argument.
Sometimes you never use the identifier because the type of the exception gives
you enough information to deal with the exception, but the identifier must
still be there.
The
handlers must appear directly after the try block. If an exception is thrown,
the exception-handling mechanism goes hunting for the first handler with an
argument that matches the type of the exception. Then it enters that catch
clause, and the exception is considered handled. (The search for handlers stops
once the catch clause is finished.) Only the matching catch clause executes;
it’s not like a
switch
statement in which you need a
break
after each
case
to prevent the remaining ones from executing.
Note
that, within the try block, a number of different method calls might generate
the same exception, but you need only one handler.
Termination
vs. resumption
There
are two basic models in exception-handling theory. In
termination
(which is what Java and C++ support), you assume the error is so critical
there’s no way to get back to where the exception occurred. Whoever threw
the exception decided that there was no way to salvage the situation, and they
don’t
want
to come back.
The
alternative is called
resumption.
It means that the exception handler is expected to do something to rectify the
situation, and then the faulting method is retried, presuming success the
second time. If you want resumption, it means you still hope to continue
execution after the exception is handled. In this case, your exception is more
like a method call – which is how you should set up situations in Java in
which you want resumption-like behavior. (That is, don’t throw an
exception; call a method that fixes the problem.) Alternatively, place your
try
block inside a
while
loop that keeps reentering the
try
block until the result is satisfactory.
Historically,
programmers using operating systems that supported resumptive exception
handling eventually ended up using termination-like code and skipping
resumption. So although resumption sounds attractive at first, it seems it
isn’t quite so useful in practice. The dominant reason is probably the coupling
that
results: your handler must often be aware of where the exception is thrown from
and contain non-generic code specific to the throwing location. This makes the
code difficult to write and maintain, especially for large systems where the
exception can be generated from many points.
The
exception specification
In
Java, you’re required to inform the client programmer, who calls your
method, of the exceptions that might be thrown from your method. This is
civilized because the caller can know exactly what code to write to catch all
potential exceptions. Of course, if source code is available, the client
programmer could hunt through and look for
throw
statements, but often a library doesn’t come with sources. To prevent
this from being a problem, Java provides syntax (and
forces
you
to use that syntax) to allow you to politely tell the client programmer what
exceptions this method throws, so the client programmer can handle them. This
is the
exception
specification
and it’s part of the method declaration, appearing after the argument list.
The
exception specification uses an additional keyword,
throws,
followed by a list of all the potential exception types. So your method
definition might look like this:
void
f() throws tooBig, tooSmall, divZero { //...
it
means that no exceptions are thrown from the method. (
Except
for
the exceptions of type
RuntimeException,
which can reasonably be thrown anywhere – this will be described later.)
You
can’t lie about an exception specification – if your method causes
exceptions and doesn’t handle them, the compiler will detect this and
tell you that you must either handle the exception or indicate with an
exception specification that it may be thrown from your method. By enforcing
exception specifications from top to bottom, Java guarantees that exception
correctness can be ensured
at
compile time
.[42] There
is one place you can lie: you can claim to throw an exception that you
don’t. The compiler takes your word for it and forces the users of your
method to treat it as if it really does throw that exception. This has the
beneficial effect of being a placeholder for that exception, so you can
actually start throwing the exception later without requiring changes to
existing code.
Catching
any exception
It
is possible to create a handler that catches any type of exception. You do this
by catching the base-class exception type
Exception
(there are other types of base exceptions, but
Exception
is the base that’s pertinent to virtually all programming activities):
catch(Exception e) {
System.out.println("caught an exception");
}
This
will catch any exception, so if you use it you’ll want to put it at the
end
of your list of handlers to avoid pre-empting any exception handlers that might
otherwise follow it.
Since
the
Exception
class is the base of all the exception classes that are important to the
programmer, you don’t get much specific information about the exception,
but you can call the methods that come from
its
base type Throwable: String
getMessage( )
Gets
the detail message.
String
toString( )
Returns
a short description of the Throwable, including the detail message if there is
one.
void
printStackTrace( )
void
printStackTrace(PrintStream)
Prints
the Throwable and the Throwable’s call stack trace. The call stack shows
the sequence of method calls that brought you to the point at which the
exception was thrown.
The
first version prints to standard error, the second prints to a stream of your
choice. If you’re working under Windows, you can’t redirect
standard error so you might want to use the second version and send the results
to
System.out;
that way the output can be redirected any way you want.
In
addition, you get some other methods from
Throwable’s
base type
Object
(everybody’s base type). The one that might come in handy for exceptions
is getClass( ),
which returns an object representing the class of this object. You can in turn
query this
Class
object for its name with
getName( )
or
toString( ).
You can also do more sophisticated things with
Class
objects that aren’t necessary in exception handling.
Class
objects will be studied later in the book.
Here’s
an example that shows the use of the
Exception
methods: (See page
97
if you have trouble executing this program.)
//: ExceptionMethods.java
// Demonstrating the Exception Methods
package c09;
public class ExceptionMethods {
public static void main(String[] args) {
try {
throw new Exception("Here's my Exception");
} catch(Exception e) {
System.out.println("Caught Exception");
System.out.println(
"e.getMessage(): " + e.getMessage());
System.out.println(
"e.toString(): " + e.toString());
System.out.println("e.printStackTrace():");
e.printStackTrace();
}
}
} ///:~
The
output for this program is:
Caught Exception
e.getMessage(): Here's my Exception
e.toString(): java.lang.Exception: Here's my Exception
e.printStackTrace():
java.lang.Exception: Here's my Exception
at ExceptionMethods.main
You
can see that the methods provide successively more information – each is
effectively a superset of the previous one.
Rethrowing
an exception
Sometimes
you’ll want to rethrow the exception that you just caught, particularly
when you use
Exception
to catch any exception. Since you already have the handle to the current
exception, you can simply re-throw that handle:
catch(Exception e) {
System.out.println("An exception was thrown");
throw e;
}
Rethrowing
an exception causes the exception to go to the exception handlers in the
next-higher context. Any further
catch
clauses for the same
try
block are still ignored. In addition, everything about the exception object is
preserved, so the handler at the higher context that catches the specific
exception type can extract all the information from that object.
If
you simply re-throw the current exception, the information that you print about
that exception in printStackTrace( )
will
pertain to the exception’s origin, not the place where you re-throw it.
If you want to install new stack trace information, you can do so by calling fillInStackTrace( ),
which returns an exception object that it creates by stuffing the current stack
information into the old exception object. Here’s what it looks like:
//: Rethrowing.java
// Demonstrating fillInStackTrace()
public class Rethrowing {
public static void f() throws Exception {
System.out.println(
"originating the exception in f()");
throw new Exception("thrown from f()");
}
public static void g() throws Throwable {
try {
f();
} catch(Exception e) {
System.out.println(
"Inside g(), e.printStackTrace()");
e.printStackTrace();
throw e; // 17
// throw e.fillInStackTrace(); // 18
}
}
public static void
main(String[] args) throws Throwable {
try {
g();
} catch(Exception e) {
System.out.println(
"Caught in main, e.printStackTrace()");
e.printStackTrace();
}
}
} ///:~
The
important line numbers are marked inside of comments. With line 17 un-commented
(as shown), the output is:
originating the exception in f()
Inside g(), e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:8)
at Rethrowing.g(Rethrowing.java:12)
at Rethrowing.main(Rethrowing.java:24)
Caught in main, e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:8)
at Rethrowing.g(Rethrowing.java:12)
at Rethrowing.main(Rethrowing.java:24)
So
the exception stack trace always remembers its true point of origin, no matter
how many times it gets rethrown.
With
line 17 commented and line 18 un-commented,
fillInStackTrace( )
is used instead, and the result is:
originating the exception in f()
Inside g(), e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:8)
at Rethrowing.g(Rethrowing.java:12)
at Rethrowing.main(Rethrowing.java:24)
Caught in main, e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.g(Rethrowing.java:18)
at Rethrowing.main(Rethrowing.java:24)
Because
of
fillInStackTrace( ),
line 18 becomes the new point
of origin of the exception.
The
class
Throwable
must appear in the exception specification for
g( )
and
main( )
because
fillInStackTrace( )
produces a handle to a
Throwable
object. Since Throwable
is a base class of
Exception,
it’s possible to get an object that’s a
Throwable
but
not
an
Exception,
so the handler for
Exception
in
main( )
might
miss it. To make sure everything is in order, the compiler forces an exception
specification for
Throwable.
For example, the exception in the following program is
not
caught in
main( ):
//: ThrowOut.java
public class ThrowOut {
public static void
main(String[] args) throws Throwable {
try {
throw new Throwable();
} catch(Exception e) {
System.out.println("Caught in main()");
}
}
} ///:~
It’s
also possible to rethrow a different exception from the one you caught. If you
do this, you get a similar effect as when you use
fillInStackTrace( ):
the information about the original site of the exception is lost, and what
you’re left with is the information pertaining to the new
throw:
//: RethrowNew.java
// Rethrow a different object from the one that
// was caught
public class RethrowNew {
public static void f() throws Exception {
System.out.println(
"originating the exception in f()");
throw new Exception("thrown from f()");
}
public static void main(String[] args) {
try {
f();
} catch(Exception e) {
System.out.println(
"Caught in main, e.printStackTrace()");
e.printStackTrace();
throw new NullPointerException("from main");
}
}
} ///:~
originating the exception in f()
Caught in main, e.printStackTrace()
java.lang.Exception: thrown from f()
at RethrowNew.f(RethrowNew.java:8)
at RethrowNew.main(RethrowNew.java:13)
java.lang.NullPointerException: from main
at RethrowNew.main(RethrowNew.java:18)
The
final exception knows only that it came from
main( ),
and not from
f( ).
Note that
Throwable
isn’t necessary in any of the exception specifications.
You
never have to worry about cleaning up the previous exception, or any exceptions
for that matter. They’re all heap-based objects created with
new,
so the garbage collector automatically cleans them all up.
[42]
This is a significant improvement over C++ exception handling, which
doesn’t catch violations of exception specifications until run time, when
it’s not very useful.