The basic philosophy of Java is
that “badly formed code will not be run.”
The ideal time to catch an
error is at compile-time, before
you even try to run the program. However, not all errors can be detected at
compile-time. The rest of the problems must be handled at run-time, through some
formality that allows the originator of the error to pass appropriate
information to a recipient who will know how to handle the difficulty
properly.
In C and other earlier languages, there
could be several of these formalities, and they were generally established by
convention and not as part of the programming language. Typically, you returned
a special value or set a flag, and the recipient was supposed to look at the
value or the flag and determine that something was amiss. However, as the years
passed, it was discovered that programmers who use a library tend to think of
themselves as invincible—as in, “Yes, errors might happen to others,
but not in my code.” So, not too surprisingly, they wouldn’t
check for the error conditions (and sometimes the error conditions were too
silly to check
for[52]). If you
were thorough enough to check for an error every time you called a
method, your code could turn into an unreadable nightmare. Because programmers
could still coax systems out of these languages they were resistant to admitting
the truth: This approach to handling errors was a major limitation to creating
large, robust, maintainable programs.
The solution is to take the casual nature
out of error handling and to enforce formality. This actually has a long
history, since implementations of exception handling go back to operating
systems in the 1960s, and even to BASIC’s “on error
goto.” But C++ exception handling was based on Ada, and Java’s
is based primarily on C++ (although it looks even more like Object
Pascal).
The word “exception” is meant
in the sense of “I take exception to that.” At the point where the
problem occurs you might not know what to do with it, but you do know that you
can’t just continue on merrily; you must stop and somebody, somewhere,
must figure out what to do. But you don’t have enough information in the
current context to fix the problem. So you hand the problem out to a higher
context where someone is qualified to make the proper decision (much like a
chain of command).
The other rather significant benefit of
exceptions is that they clean up error handling code. Instead of checking for a
particular error and dealing with it at multiple places in your program, you no
longer need to check at the point of the method call (since the exception will
guarantee that someone catches it). And, you need to handle the problem in only
one place, the so-called exception handler. This
saves you code, and it separates the code that describes what you want to do
from the code that is executed when things go awry. In general, reading,
writing, and debugging code becomes much clearer with exceptions than when using
the old way of error handling.
Because exception handling is enforced by
the Java compiler, there are only so many examples that can be written in this
book without learning about exception handling. This chapter introduces you to
the code you need to write to properly handle exceptions, and the way you can
generate your own exceptions if one of your methods gets into
trouble.
An exceptional
condition is a problem that prevents the continuation of the method or scope
that you’re in. It’s important to distinguish an exceptional
condition from a normal problem, in which you have enough information in the
current context to somehow cope with the difficulty. With an exceptional
condition, you cannot continue processing because you don’t have the
information necessary to deal with the problem in the current context.
All you can do is jump out of the current context and relegate that problem to a
higher context. This is what happens when you throw an
exception.
A simple example is a divide. If
you’re about to divide by zero, it’s worth checking to make sure you
don’t go ahead and perform the divide. But what does it mean that the
denominator is zero? Maybe you know, in the context of the problem you’re
trying to solve in that particular method, how to deal with a zero denominator.
But if it’s an unexpected value, you can’t deal with it and so must
throw an exception rather than continuing along that path.
When you throw an
exception, several things happen. First, the exception
object is created in the same way that any Java object is created: on the heap,
with new. Then the current path of execution (the one you couldn’t
continue) is stopped and the reference for the exception object is ejected from
the current context. At this point the exception handling mechanism takes over
and begins to look for an appropriate place to continue executing the program.
This appropriate place is the exception handler, whose job is to recover
from the problem so the program can either try another tack or just
continue.
As a simple example of throwing an
exception, consider an object reference called t. It’s possible
that you might be passed a reference that hasn’t been initialized, so you
might want to check before trying to call a method using that object reference.
You can send information about the error into a larger context by creating an
object representing your information and “throwing” it out of your
current context. This is called throwing an exception. Here’s what
it looks like:
if(t == null) throw new NullPointerException();
This throws the exception, which allows
you—in the current context—to abdicate responsibility for thinking
about the issue further. It’s just magically handled somewhere else.
Precisely where will be shown
shortly.
Like any object in Java, you always
create exceptions on the heap using new, which allocates storage and
calls a constructor. There are two constructors in all standard exceptions: the
first is the default constructor, and the second takes a string argument so you
can place pertinent information in the exception:
if(t == null) throw new NullPointerException("t = null");
This string can later be extracted using
various methods, as will be shown shortly.
The keyword
throw causes a number of relatively magical things to happen. Typically,
you’ll first use new to create an object that represents the
error condition. You give the resulting reference to throw. The object
is, in effect, “returned” from the method, even though that object
type isn’t normally what the method is designed to return. A simplistic
way to think about exception handling is as an alternate return mechanism,
although you get into trouble if you take that analogy too far. You can also
exit from ordinary scopes by throwing an exception. But a value is returned, and
the method or scope exits.
Any similarity to an ordinary return from
a method ends here, because where you return is someplace completely
different from where you return for a normal method call. (You end up in an
appropriate exception handler that might be miles away—many levels lower
on the call stack—from where the exception was thrown.)
In addition, you can throw any type of
Throwable object that you want. Typically, you’ll throw a different
class of exception for each different type of error. The information about the
error is represented both inside the exception object and implicitly in the type
of exception object chosen, so someone in the bigger context can figure out what
to do with your exception. (Often, the only information is the type of exception
object, and nothing meaningful is stored within the exception
object.)
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 which is followed by the
code to handle those
exceptions.
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 exit the 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.
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.
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 that 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 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 nongeneric 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.
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 { //...
If you say
void f() { // ...
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[53].
There is one place you can lie: you can
claim to throw an exception that you really 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. It’s also important for
creating abstract base classes and interfaces whose derived
classes or implementations may need to throw
exceptions.
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 preempting 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( )
String
getLocalizedMessage( )
Gets
the detail message, or a message adjusted for this particular
locale.
String
toString( )
Returns a short description
of the Throwable, including the detail message if there is one.
void
printStackTrace( )
void
printStackTrace(PrintStream)
void
printStackTrace(PrintWriter)
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 and third prints to a stream of
your choice (in Chapter 11, you’ll understand why there are two types of
streams).
Throwable
fillInStackTrace( )
Records
within this Throwable object information about the current state of the
stack frames. Useful when an application is rethrowing an error or exception
(more about this shortly).
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 this book.
Here’s an example that shows the
use of the basic Exception methods:
//: c10:ExceptionMethods.java // Demonstrating the Exception Methods. 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.getLocalizedMessage(): " + e.getLocalizedMessage()); System.out.println("e.toString(): " + e); System.out.println("e.printStackTrace():"); e.printStackTrace(); e.printStackTrace(System.err); } } } ///:~
The output for this program
is:
Caught Exception e.getMessage(): Here's my Exception e.getLocalizedMessage(): 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(ExceptionMethods.java:7) java.lang.Exception: Here's my Exception at ExceptionMethods.main(ExceptionMethods.java:7)
You can see that the methods provide
successively more information—each is effectively a superset of the
previous
one.
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 reference to the current
exception, you can simply rethrow that reference:
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 rethrow 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
rethrow 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:
//: c10: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 as
comments. With line 17 uncommented (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
uncommented, 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)
The class Throwable must appear in
the exception specification for g( ) and main( ) because
fillInStackTrace( ) produces a reference 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( ):
//: c10: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:
//: c10: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"); } } } ///:~
The output is:
originating the exception in f() Caught in main, e.printStackTrace() java.lang.Exception: thrown from f() at RethrowNew.f(RethrowNew.java:9) at RethrowNew.main(RethrowNew.java:13) Exception in thread "main" 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.
The Java class Throwable describes
anything that can be thrown as an exception. There are two general types of
Throwable objects (“types of” = “inherited
from”). Error represents compile-time and
system errors that you don’t worry about catching (except in special
cases). Exception is the basic type that can be
thrown from any of the standard Java library class methods and from your methods
and run-time accidents. So the Java programmer’s base type of interest is
Exception.
The best way to get an overview of the
exceptions is to browse online Java documentation at java.sun.com. (Of
course, it’s easier to download it first.) It’s worth doing this
once just to get a feel for the various exceptions, but you’ll soon see
that there isn’t anything special between one exception and the next
except for the name. Also, the number of exceptions in Java keeps expanding;
basically it’s pointless to print them in a book. Any new library you get
from a third-party vendor will probably have its own exceptions as well. The
important thing to understand is the concept and what you should do with the
exceptions.
java.lang.Exception
This is the basic exception class your
program will catch. Other exceptions are derived from this. The basic idea is
that the name of the exception represents the problem that occurred, and the
exception name is intended to be relatively self-explanatory. The exceptions are
not all defined in java.lang; some are created to support other libraries
such as util, net, and io, which you can see from their
full class names or what they are inherited from. For example, all I/O
exceptions are inherited from
java.io.IOException.
The first example in this chapter
was
if(t == null) throw new NullPointerException();
It can be a bit horrifying to think that
you must check for null on every reference that is passed into a method
(since you can’t know if the caller has passed you a valid reference).
Fortunately, you don’t—this is part of the standard run-time
checking that Java performs for you, and if any call is made to a null
reference, Java will automatically throw a
NullPointerException. So
the above bit of code is always superfluous.
There’s a whole group of exception
types that are in this category. They’re always thrown automatically by
Java and you don’t need to include them in your exception specifications.
Conveniently enough, they’re all grouped together by putting them under a
single base class called RuntimeException, which is a perfect example of
inheritance: it establishes a family of types that have some characteristics and
behaviors in common. Also, you never need to write an exception specification
saying that a method might throw a RuntimeException, since that’s
just assumed. Because they indicate bugs, you virtually never catch a
RuntimeException—it’s
dealt with automatically. If you were forced to check for
RuntimeExceptions your code could get messy. Even though you don’t
typically catch RuntimeExceptions, in your own packages you might
choose to throw some of the RuntimeExceptions.
What happens when you don’t catch
such exceptions? Since the compiler doesn’t enforce exception
specifications for these, it’s quite plausible that a
RuntimeException could percolate all the way out to your main( )
method without being caught. To see what happens in this case, try the
following example:
//: c10:NeverCaught.java // Ignoring RuntimeExceptions. public class NeverCaught { static void f() { throw new RuntimeException("From f()"); } static void g() { f(); } public static void main(String[] args) { g(); } } ///:~
You can already see that a
RuntimeException (or anything inherited from it) is a special case, since
the compiler doesn’t require an exception specification for these
types.
The output is:
Exception in thread "main" java.lang.RuntimeException: From f() at NeverCaught.f(NeverCaught.java:9) at NeverCaught.g(NeverCaught.java:12) at NeverCaught.main(NeverCaught.java:15)
So the answer is: If a
RuntimeException gets all the way out to main( ) without
being caught, printStackTrace( ) is called for that exception as the
program exits.
Keep in mind that it’s possible to
ignore only RuntimeExceptions in your coding, since all other handling is
carefully enforced by the compiler. The reasoning is that a
RuntimeException represents a programming error:
You
can see what a tremendous benefit it is to have exceptions in this case, since
they help in the debugging process.
It’s interesting to notice that you
cannot classify Java exception handling as a single-purpose tool. Yes, it is
designed to handle those pesky run-time errors that will occur because of forces
outside your code’s control, but it’s also essential for certain
types of programming bugs that the compiler cannot
detect.
You’re not stuck using the Java
exceptions. This is important because you’ll often
need to create your own exceptions to denote a special error that your library
is capable of creating, but which was not foreseen when the Java exception
hierarchy was created.
To create your own exception class,
you’re forced to inherit from an existing type of exception, preferably
one that is close in meaning to your new exception (this is often not possible,
however). Inheriting an exception is quite simple:
//: c10:Inheriting.java // Inheriting your own exceptions. class MyException extends Exception { public MyException() {} public MyException(String msg) { super(msg); } } public class Inheriting { public static void f() throws MyException { System.out.println( "Throwing MyException from f()"); throw new MyException(); } public static void g() throws MyException { System.out.println( "Throwing MyException from g()"); throw new MyException("Originated in g()"); } public static void main(String[] args) { try { f(); } catch(MyException e) { e.printStackTrace(); } try { g(); } catch(MyException e) { e.printStackTrace(); } } } ///:~
The inheritance occurs in the creation of
the new class:
class MyException extends Exception { public MyException() {} public MyException(String msg) { super(msg); } }
The key phrase here is extends
Exception, which says “it’s everything an Exception is
and more.” The added code is small—the addition of two constructors
that define the way MyException is created. Remember that the compiler
automatically calls the base-class default constructor if you don’t
explicitly call a base-class constructor, as in the MyException( )
default constructor. In the second constructor, the base-class constructor with
a String argument is explicitly invoked by using the super
keyword.
The output of the program
is:
Throwing MyException from f() MyException at Inheriting.f(Inheriting.java:16) at Inheriting.main(Inheriting.java:24) Throwing MyException from g() MyException: Originated in g() at Inheriting.g(Inheriting.java:20) at Inheriting.main(Inheriting.java:29)
You can see the absence of the detail
message in the MyException thrown from f( ).
The process of creating your own
exceptions can be taken further. You can add extra constructors and
members:
//: c10:Inheriting2.java // Inheriting your own exceptions. class MyException2 extends Exception { public MyException2() {} public MyException2(String msg) { super(msg); } public MyException2(String msg, int x) { super(msg); i = x; } public int val() { return i; } private int i; } public class Inheriting2 { public static void f() throws MyException2 { System.out.println( "Throwing MyException2 from f()"); throw new MyException2(); } public static void g() throws MyException2 { System.out.println( "Throwing MyException2 from g()"); throw new MyException2("Originated in g()"); } public static void h() throws MyException2 { System.out.println( "Throwing MyException2 from h()"); throw new MyException2( "Originated in h()", 47); } public static void main(String[] args) { try { f(); } catch(MyException2 e) { e.printStackTrace(); } try { g(); } catch(MyException2 e) { e.printStackTrace(); } try { h(); } catch(MyException2 e) { e.printStackTrace(); System.out.println("e.val() = " + e.val()); } } } ///:~
A data member i has been added,
along with a method that reads that value and an additional constructor that
sets it. The output is:
Throwing MyException2 from f() MyException2 at Inheriting2.f(Inheriting2.java:22) at Inheriting2.main(Inheriting2.java:34) Throwing MyException2 from g() MyException2: Originated in g() at Inheriting2.g(Inheriting2.java:26) at Inheriting2.main(Inheriting2.java:39) Throwing MyException2 from h() MyException2: Originated in h() at Inheriting2.h(Inheriting2.java:30) at Inheriting2.main(Inheriting2.java:44) e.val() = 47
Since an exception is just another kind
of object, you can continue this process of embellishing the power of your
exception classes. Keep in mind, however, that all this dressing-up might be
lost on the client programmers using your packages, since they might simply look
for the exception to be thrown and nothing more. (That’s the way most of
the Java library exceptions are used.) If this is the case, it’s possible
to create a new exception type with almost no code at all:
//: c10:SimpleException.java class SimpleException extends Exception { } ///:~
This relies on the compiler to create the
default constructor (which automatically calls the base-class default
constructor). Of course, in this case you don’t get a
SimpleException(String) constructor, but in practice that isn’t
used
much.
When you override a method, you can throw
only the exceptions that have been specified in the base-class version of the
method. This is a useful restriction, since it means that code that works with
the base class will automatically work with any object derived from the base
class (a fundamental OOP concept, of course), including
exceptions.
This example demonstrates the kinds of
restrictions imposed (at compile-time) for exceptions:
//: c10:StormyInning.java // Overridden methods may throw only the // exceptions specified in their base-class // versions, or exceptions derived from the // base-class exceptions. class BaseballException extends Exception {} class Foul extends BaseballException {} class Strike extends BaseballException {} abstract class Inning { Inning() throws BaseballException {} void event () throws BaseballException { // Doesn't actually have to throw anything } abstract void atBat() throws Strike, Foul; void walk() {} // Throws nothing } class StormException extends Exception {} class RainedOut extends StormException {} class PopFoul extends Foul {} interface Storm { void event() throws RainedOut; void rainHard() throws RainedOut; } public class StormyInning extends Inning implements Storm { // OK to add new exceptions for // constructors, but you must deal // with the base constructor exceptions: StormyInning() throws RainedOut, BaseballException {} StormyInning(String s) throws Foul, BaseballException {} // Regular methods must conform to base class: //! void walk() throws PopFoul {} //Compile error // Interface CANNOT add exceptions to existing // methods from the base class: //! public void event() throws RainedOut {} // If the method doesn't already exist in the // base class, the exception is OK: public void rainHard() throws RainedOut {} // You can choose to not throw any exceptions, // even if base version does: public void event() {} // Overridden methods can throw // inherited exceptions: void atBat() throws PopFoul {} public static void main(String[] args) { try { StormyInning si = new StormyInning(); si.atBat(); } catch(PopFoul e) { } catch(RainedOut e) { } catch(BaseballException e) {} // Strike not thrown in derived version. try { // What happens if you upcast? Inning i = new StormyInning(); i.atBat(); // You must catch the exceptions from the // base-class version of the method: } catch(Strike e) { } catch(Foul e) { } catch(RainedOut e) { } catch(BaseballException e) {} } } ///:~
In Inning, you can see that both
the constructor and the event( ) method say they will throw an
exception, but they never do. This is legal because it allows you to force the
user to catch any exceptions that might be added in overridden versions of
event( ). The same idea holds for abstract methods, as seen
in atBat( ).
The interface Storm is interesting
because it contains one method (event( )) that is defined in
Inning, and one method that isn’t. Both methods throw a new type of
exception, RainedOut. When StormyInning extends Inning and
implements Storm, you’ll see that the event( ) method
in Storm cannot change the exception interface of
event( ) in Inning. Again, this makes sense because otherwise
you’d never know if you were catching the correct thing when working with
the base class. Of course, if a method described in an interface is not
in the base class, such as rainHard( ), then there’s no
problem if it throws exceptions.
The restriction on exceptions does not
apply to constructors. You can
see in StormyInning that a constructor can throw anything it wants,
regardless of what the base-class constructor throws. However, since a
base-class constructor must always be called one way or another (here, the
default constructor is called automatically), the derived-class constructor must
declare any base-class constructor exceptions in its exception specification.
Note that a derived-class constructor cannot catch exceptions thrown by its
base-class constructor.
The reason
StormyInning.walk( ) will not compile is that it throws an
exception, while Inning.walk( ) does not. If this was allowed, then
you could write code that called Inning.walk( ) and that
didn’t have to handle any exceptions, but then when you substituted an
object of a class derived from Inning, exceptions would be thrown so your
code would break. By forcing the derived-class methods to conform to the
exception specifications of the base-class methods, substitutability of objects
is maintained.
The overridden event( )
method shows that a derived-class version of a method may choose not to throw
any exceptions, even if the base-class version does. Again, this is fine since
it doesn’t break any code that is written—assuming the base-class
version throws exceptions. Similar logic applies to atBat( ), which
throws PopFoul, an exception that is derived from Foul thrown by
the base-class version of atBat( ). This way, if someone writes code
that works with Inning and calls atBat( ), they must catch
the Foul exception. Since PopFoul is derived from Foul, the
exception handler will also catch PopFoul.
The last point of interest is in
main( ). Here you can see that if you’re dealing with exactly
a StormyInning object, the compiler forces you to catch only the
exceptions that are specific to that class, but if you upcast to the base type
then the compiler (correctly) forces you to catch the exceptions for the base
type. All these constraints produce much more robust exception-handling
code[54].
It’s useful to realize that
although exception specifications are enforced by the compiler during
inheritance, the exception specifications are not part of the type of a method,
which is comprised of only the method name and argument types. Therefore, you
cannot overload methods based on exception specifications. In addition, just
because an exception specification exists in a base-class version of a method
doesn’t mean that it must exist in the derived-class version of the
method. This is quite different from inheritance rules, where a method in the
base class must also exist in the derived class. Put another way, the
“exception specification interface” for a particular method may
narrow during inheritance and overriding, but it may not widen—this is
precisely the opposite of the rule for the class interface during
inheritance.
There’s often some piece of code
that you want to execute whether or not an exception is thrown within a
try block. This usually pertains to some operation other than memory
recovery (since that’s taken care of by the garbage collector). To achieve
this effect, you use a
finally
clause[55] at the
end of all the exception handlers. The full picture of an exception handling
section is thus:
try { // The guarded region: Dangerous activities // that might throw A, B, or C } catch (A a1) { // Handler for situation A } catch (B b1) { // Handler for situation B } catch (C c1) { // Handler for situation C } finally { // Activities that happen every time }
To demonstrate that the finally
clause always runs, try this program:
//: c10:FinallyWorks.java // The finally clause is always executed. public class FinallyWorks { static int count = 0; public static void main(String[] args) { while(true) { try { // Post-increment is zero first time: if(count++ == 0) throw new Exception(); System.out.println("No exception"); } catch(Exception e) { System.out.println("Exception thrown"); } finally { System.out.println("In finally clause"); if(count == 2) break; // out of "while" } } } } ///:~
This program also gives a hint for how
you can deal with the fact that exceptions in Java (like exceptions in C++) do
not allow you to resume back to where the exception was thrown, as discussed
earlier. If you place your try block in a loop, you can establish a
condition that must be met before you continue the program. You can also add a
static counter or some other device to allow the loop to try several
different approaches before giving up. This way you can build a greater level of
robustness into your programs.
The output is:
Exception thrown In finally clause No exception In finally clause
In a language without garbage collection
and without automatic destructor
calls[56],
finally is important because it allows the programmer to guarantee the
release of memory regardless of what happens in the
try block. But Java has
garbage collection, so releasing memory is virtually never a problem. Also, it
has no destructors to call. So when do you need to use
finally in Java?
finally is necessary when you need
to set something other than memory back to its original state. This is
usually something like an open file or network connection, something
you’ve drawn on the screen, or even a switch in the outside world, as
modeled in the following example:
//: c10:Switch.java public class Switch { boolean state = false; boolean read() { return state; } void on() { state = true; } void off() { state = false; } } ///:~
//: c10:OnOffSwitch.java // Why use finally? public class OnOffSwitch { static Switch sw = new Switch(); public static void main(String[] args) { try { sw.on(); // Code that can throw exceptions... sw.off(); } catch(NullPointerException e) { System.out.println("NullPointerException"); sw.off(); } catch(IllegalArgumentException e) { System.out.println("IOException"); sw.off(); } } } ///:~
The goal here is to make sure that the
switch is off when main( ) is completed, so sw.off( ) is
placed at the end of the try block and at the end of each exception handler. But
it’s possible that an exception could be thrown that isn’t caught
here, so sw.off( ) would be missed. However, with finally you
can place the cleanup code from a try block in just one place:
//: c10:WithFinally.java // Finally Guarantees cleanup. public class WithFinally { static Switch sw = new Switch(); public static void main(String[] args) { try { sw.on(); // Code that can throw exceptions... } catch(NullPointerException e) { System.out.println("NullPointerException"); } catch(IllegalArgumentException e) { System.out.println("IOException"); } finally { sw.off(); } } } ///:~
Here the sw.off( ) has been
moved to just one place, where it’s guaranteed to run no matter what
happens.
Even in cases in which the exception is
not caught in the current set of catch clauses, finally will be
executed before the exception handling mechanism continues its search for a
handler at the next higher level:
//: c10:AlwaysFinally.java // Finally is always executed. class Ex extends Exception {} public class AlwaysFinally { public static void main(String[] args) { System.out.println( "Entering first try block"); try { System.out.println( "Entering second try block"); try { throw new Ex(); } finally { System.out.println( "finally in 2nd try block"); } } catch(Ex e) { System.out.println( "Caught Ex in first try block"); } finally { System.out.println( "finally in 1st try block"); } } } ///:~
The output for this program shows you
what happens:
Entering first try block Entering second try block finally in 2nd try block Caught Ex in first try block finally in 1st try block
The finally statement will also be
executed in situations in which break and continue statements are
involved. Note that, along with the labeled break and labeled
continue, finally eliminates the need for a goto statement
in Java.
In general, Java’s exception
implementation is quite outstanding, but unfortunately there’s a flaw.
Although exceptions are an indication of a crisis in your program and should
never be ignored, it’s possible for an exception to simply be
lost. This happens with a particular configuration using
a finally clause:
//: c10:LostMessage.java // How an exception can be lost. class VeryImportantException extends Exception { public String toString() { return "A very important exception!"; } } class HoHumException extends Exception { public String toString() { return "A trivial exception"; } } public class LostMessage { void f() throws VeryImportantException { throw new VeryImportantException(); } void dispose() throws HoHumException { throw new HoHumException(); } public static void main(String[] args) throws Exception { LostMessage lm = new LostMessage(); try { lm.f(); } finally { lm.dispose(); } } } ///:~
The output is:
Exception in thread "main" A trivial exception at LostMessage.dispose(LostMessage.java:21) at LostMessage.main(LostMessage.java:29)
You can see that there’s no
evidence of the VeryImportantException, which is simply replaced by the
HoHumException in the finally clause. This is a rather serious
pitfall, since it means that an exception can be completely lost, and in a far
more subtle and difficult-to-detect fashion than the example above. In contrast,
C++ treats the situation in which a second exception is thrown before the first
one is handled as a dire programming error. Perhaps a future version of Java
will repair this
problem.
When writing code with exceptions,
it’s particularly important that you always ask, “If an exception
occurs, will this be properly cleaned up?” Most of the time you’re
fairly safe, but in constructors there’s a problem. The constructor puts
the object into a safe starting state, but it might perform some
operation—such as opening a file—that doesn’t get cleaned up
until the user is finished with the object and calls a special cleanup method.
If you throw an exception from inside a constructor, these cleanup behaviors
might not occur properly. This means that you must be especially diligent while
you write your constructor.
Since you’ve just learned about
finally, you might think
that it is the correct solution. But it’s not quite that simple, because
finally performs the cleanup code every time, even in the
situations in which you don’t want the cleanup code executed until the
cleanup method runs. Thus, if you do perform cleanup in finally, you must
set some kind of flag when the constructor finishes normally so that you
don’t do anything in the finally block if the flag is set. Because
this isn’t particularly elegant (you are coupling your code from one place
to another), it’s best if you try to avoid performing this kind of cleanup
in finally unless you are forced to.
In the following example, a class called
InputFile is created that opens a file and allows you to read it one line
(converted into a String) at a time. It uses the classes
FileReader and
BufferedReader from the
Java standard I/O library that will be discussed in Chapter 11, but which are
simple enough that you probably won’t have any trouble understanding their
basic use:
//: c10:Cleanup.java // Paying attention to exceptions // in constructors. import java.io.*; class InputFile { private BufferedReader in; InputFile(String fname) throws Exception { try { in = new BufferedReader( new FileReader(fname)); // Other code that might throw exceptions } catch(FileNotFoundException e) { System.out.println( "Could not open " + fname); // Wasn't open, so don't close it throw e; } catch(Exception e) { // All other exceptions must close it try { in.close(); } catch(IOException e2) { System.out.println( "in.close() unsuccessful"); } throw e; } finally { // Don't close it here!!! } } String getLine() { String s; try { s = in.readLine(); } catch(IOException e) { System.out.println( "readLine() unsuccessful"); s = "failed"; } return s; } void cleanup() { try { in.close(); } catch(IOException e2) { System.out.println( "in.close() unsuccessful"); } } } public class Cleanup { public static void main(String[] args) { try { InputFile in = new InputFile("Cleanup.java"); String s; int i = 1; while((s = in.getLine()) != null) System.out.println(""+ i++ + ": " + s); in.cleanup(); } catch(Exception e) { System.out.println( "Caught in main, e.printStackTrace()"); e.printStackTrace(); } } } ///:~
The constructor for InputFile
takes a String argument, which is the name of the file you want to open.
Inside a try block, it creates a FileReader using the file name. A
FileReader isn’t particularly useful until you turn around and use
it to create a BufferedReader that you can actually talk to—notice
that one of the benefits of InputFile is that it combines these two
actions.
If the FileReader constructor is
unsuccessful, it throws a
FileNotFoundException,
which must be caught separately because that’s the one case in which you
don’t want to close the file since it wasn’t successfully opened.
Any other catch clauses must close the file because it was opened
by the time those catch clauses are entered. (Of course, this is trickier if
more than one method can throw a FileNotFoundException. In that case, you
might want to break things into several try blocks.) The
close( ) method might throw an exception so it is tried and caught
even though it’s within the block of another catch
clause—it’s just another pair of curly braces to the Java compiler.
After performing local operations, the exception is rethrown, which is
appropriate because this constructor failed, and you wouldn’t want the
calling method to assume that the object had been properly created and was
valid.
In this example, which doesn’t use
the aforementioned flagging technique, the finally clause is definitely
not the place to close( ) the file, since that would close it
every time the constructor completed. Since we want the file to be open for the
useful lifetime of the InputFile object this would not be
appropriate.
The getLine( ) method returns
a String containing the next line in the file. It calls
readLine( ), which
can throw an exception, but that exception is caught so getLine( )
doesn’t throw any exceptions. One of the design issues with
exceptions is whether to handle an exception completely
at this level, to handle it partially and pass the same exception (or a
different one) on, or whether to simply pass it on. Passing it on, when
appropriate, can certainly simplify coding. The getLine( ) method
becomes:
String getLine() throws IOException { return in.readLine(); }
But of course, the caller is now
responsible for handling any IOException that might
arise.
The cleanup( ) method must be
called by the user when finished using the InputFile object. This will
release the system resources (such as file handles) that are used by the
BufferedReader and/or FileReader
objects[57]. You
don’t want to do this until you’re finished with the
InputFile object, at the point you’re going to let it go. You might
think of putting such functionality into a
finalize( ) method, but as mentioned in
Chapter 4 you can’t always be sure that finalize( ) will be
called (even if you can be sure that it will be called, you don’t
know when). This is one of the downsides to Java: all cleanup—other
than memory cleanup—doesn’t happen automatically, so you must inform
the client programmer that they are responsible, and possibly guarantee that
cleanup occurs using finalize( ).
In Cleanup.java an
InputFile is created to open the same source file that creates the
program, the file is read in a line at a time, and line numbers are added. All
exceptions are caught generically in main( ), although you could
choose greater granularity.
One of the benefits of this example is to
show you why exceptions are introduced at this point in the book—you
can’t do basic I/O without using exceptions. Exceptions are so integral to
programming in Java, especially because the compiler enforces them, that you can
accomplish only so much without knowing how to work with
them.
When an exception is thrown, the
exception handling system looks through the “nearest” handlers in
the order they are written. When it finds a match, the exception is considered
handled, and no further searching occurs.
Matching an exception doesn’t
require a perfect match between the exception and its handler. A derived-class
object will match a handler for the base class, as shown in
this example:
//: c10:Human.java // Catching exception hierarchies. class Annoyance extends Exception {} class Sneeze extends Annoyance {} public class Human { public static void main(String[] args) { try { throw new Sneeze(); } catch(Sneeze s) { System.out.println("Caught Sneeze"); } catch(Annoyance a) { System.out.println("Caught Annoyance"); } } } ///:~
The Sneeze exception will be
caught by the first catch clause that it matches—which is the first
one, of course. However, if you remove the first catch clause, leaving
only:
try { throw new Sneeze(); } catch(Annoyance a) { System.out.println("Caught Annoyance"); }
The code will still work because
it’s catching the base class of Sneeze. Put another way,
catch(Annoyance e) will catch an Annoyance or any class derived
from it. This is useful because if you decide to add more derived
exceptions to a method, then the client programmer’s code will not need
changing as long as the client catches the base class
exceptions.
If you try to “mask” the
derived-class exceptions by putting the base-class catch clause first, like
this:
try { throw new Sneeze(); } catch(Annoyance a) { System.out.println("Caught Annoyance"); } catch(Sneeze s) { System.out.println("Caught Sneeze"); }
the compiler will give you an error
message, since it sees that the Sneeze catch-clause can never be
reached.
Use exceptions to:
Improved error
recovery is one of the most powerful ways that you can
increase the robustness of your code. Error recovery is a fundamental concern
for every program you write, but it’s especially important in Java, where
one of the primary goals is to create program components for others to use.
To create a robust system, each component must be
robust.
The goals for exception handling in Java
are to simplify the creation of large, reliable programs using less code than
currently possible, and with more confidence that your application doesn’t
have an unhandled error.
Exceptions are not terribly difficult to learn, and are one of those features that provide immediate and significant benefits to your project. Fortunately, Java enforces all aspects of exceptions so it’s guaranteed that they will be used consistently by both library designers and client programmers."_Toc375545380">"_Toc479507638">
[52]
The C programmer can look up the return value of printf( ) for an
example of this.
[53]
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.
[54]
ISO C++ added similar constraints that require derived-method exceptions to be
the same as, or derived from, the exceptions thrown by the base-class method.
This is one case in which C++ is actually able to check exception specifications
at compile-time.
[55]
C++ exception handling does not have the finally clause because it relies
on destructors to accomplish this sort of cleanup.
[56]
A destructor is a function that’s always called when an object becomes
unused. You always know exactly where and when the destructor gets called. C++
has automatic destructor calls, but Delphi’s Object Pascal versions 1 and
2 do not (which changes the meaning and use of the concept of a destructor for
that language).
[57]
In C++, a destructor would handle this for you.