|
|
|
Sponsored Link •
|
|
Advertisement
|
Summary
This tutorial covers the nuts and bolts of what exceptions are and how they work in the Java language and virtual machine. It discusses exception classes and objects, throwing and catching exceptions, the method invocation stack, thethrowsclause, checked vs. unchecked exceptions, andfinallyclauses.
Exceptions are the customary way in Java to indicate to a calling method that an abnormal condition has occurred. This article is a companion piece to this month's Design Techniques installment, which discusses how to use exceptions appropriately in your programs and designs. Look to this companion article for a tutorial on the nuts and bolts of what exceptions are and how they work in the Java language and virtual machine.
When a method encounters an abnormal condition (an exception condition) that it can't handle itself, it may throw an exception. Throwing an exception is like throwing a beeping, flashing red ball to indicate there is a problem that can't be handled where it occurred. Somewhere, you hope, this ball will be caught and the problem will be dealt with. Exceptions are caught by handlers positioned along the thread's method invocation stack. If the calling method isn't prepared to catch the exception, it throws the exception up to its calling method, and so on. If one of the threads of your program throws an exception that isn't caught by any method along the method invocation stack, that thread will expire. When you program in Java, you must position catchers (the exception handlers) strategically, so your program will catch and handle all exceptions from which you want your program to recover.
Exception classes
In Java, exceptions are objects. When you throw an exception, you
throw an object. You can't throw just any object as an exception,
however -- only those objects whose classes descend from
Throwable. Throwable serves as the base
class for an entire family of classes, declared in
java.lang, that your program can instantiate and
throw. A small part of this family is shown in Figure 1.
As you can see in Figure 1, Throwable has two
direct subclasses, Exception and Error.
Exceptions (members of the Exception family) are
thrown to signal abnormal conditions that can often be handled by
some catcher, though it's possible they may not be caught and
therefore could result in a dead thread. Errors (members of the
Error family) are usually thrown for more serious
problems, such as OutOfMemoryError, that may not be
so easy to handle. In general, code you write should throw only
exceptions, not errors. Errors are usually thrown by the methods
of the Java API, or by the Java virtual machine itself.
![]() Figure 1. A partial view of the Throwable family
|
In addition to throwing objects whose classes are declared in
java.lang, you can throw objects of your own design.
To create your own class of throwable objects, you need only
declare it as a subclass of some member of the
Throwable family. In general, however, the throwable
classes you define should extend class Exception.
They should be "exceptions." The reasoning behind this rule will
be explained later in this article.
Whether you use an existing exception class from
java.lang or create one of your own depends upon the
situation. In some cases, a class from java.lang
will do just fine. For example, if one of your methods is invoked
with an invalid argument, you could throw
IllegalArgumentException, a subclass of
RuntimeException in java.lang.
Other times, however, you will want to convey more information
about the abnormal condition than a class from
java.lang will allow. Usually, the class of the
exception object itself indicates the type of abnormal condition
that was encountered. For example, if a thrown exception object
has class IllegalArgumentException, that indicates
someone passed an illegal argument to a method. Sometimes you
will want to indicate that a method encountered an abnormal
condition that isn't represented by a class in the
Throwable family of java.lang.
As an example, imagine you are writing a Java program that simulates a customer of a virtual café drinking a cup of coffee. Consider the exceptional conditions that might occur while the customer sips. The class hierarchy of exceptions shown in Figure 2 represents a few possibilities.
![]() Figure 2. Exception hierarchy for coffee sipping |
If the customer discovers, with dismay, that the coffee is
cold, your program could throw a TooColdException.
On the other hand, if the customer discovers that the coffee is
overly hot, your program could throw a
TooHotException. These conditions could be
exceptions because they are (hopefully) not the normal
situation in your café. (Exceptional conditions are not
necessarily rare, just outside the normal flow of events.) The
code for your new exception classes might look like this:
// In Source Packet in file except/ex1/TemperatureException.java
class TemperatureException extends Exception {
}
// In Source Packet in file except/ex1/TooColdException.java
class TooColdException extends TemperatureException {
}
// In Source Packet in file except/ex1/TooHotException.java
class TooHotException extends TemperatureException {
}
This family of classes, the TemperatureException
family, declares three new types of exceptions for your program
to throw. Note that each exception indicates by its class the
kind of abnormal condition that would cause it to be thrown:
TemperatureException indicates some kind of problem
with temperature; TooColdException indicates
something was too cold; and TooHotException
indicates something was too hot. Note also that
TemperatureException extends Exception
-- not Throwable, Error, or any other
class declared in java.lang.
Throwing
exceptions
To throw an exception, you simply use the throw
keyword with an object reference, as in:
throw new TooColdException();
The type of the reference must be Throwable or
one of its subclasses.
The following code shows how a class that represents the
customer, class VirtualPerson, might throw
exceptions if the coffee didn't meet the customer's temperature
preferences. Note that Java also has a throws
keyword in addition to the throw keyword. Only
throw can be used to throw an exception. The meaning
of throws will be explained later in this
article.
// In Source Packet in file except/ex1/VirtualPerson.java
class VirtualPerson {
private static final int tooCold = 65;
private static final int tooHot = 85;
public void drinkCoffee(CoffeeCup cup) throws
TooColdException, TooHotException {
int temperature = cup.getTemperature();
if (temperature <= tooCold) {
throw new TooColdException();
}
else if (temperature >= tooHot) {
throw new TooHotException();
}
//...
}
//...
}
// In Source Packet in file except/ex1/CoffeeCup.java
class CoffeeCup {
// 75 degrees Celsius: the best temperature for coffee
private int temperature = 75;
public void setTemperature(int val) {
temperature = val;
}
public int getTemperature() {
return temperature;
}
//...
}
Catching
exceptions
To catch an exception in Java, you write a try block
with one or more catch clauses. Each
catch clause specifies one exception type that it is
prepared to handle. The try block places a fence
around a bit of code that is under the watchful eye of the
associated catchers. If the bit of code delimited by the
try block throws an exception, the associated
catch clauses will be examined by the Java virtual
machine. If the virtual machine finds a catch clause
that is prepared to handle the thrown exception, the program
continues execution starting with the first statement of that
catch clause.
As an example, consider a program that requires one argument
on the command line, a string that can be parsed into an integer.
When you have a String and want an int,
you can invoke the parseInt() method of the
Integer class. If the string you pass represents an
integer, parseInt() will return the value. If the
string doesn't represent an integer, parseInt()
throws NumberFormatException. Here is how you might
parse an int from a command-line argument:
// In Source Packet in file except/ex1/Example1.java
class Example1 {
public static void main(String[] args) {
int temperature = 0;
if (args.length > 0) {
try {
temperature = Integer.parseInt(args[0]);
}
catch(NumberFormatException e) {
System.out.println(
"Must enter integer as first argument.");
return;
}
}
else {
System.out.println(
"Must enter temperature as first argument.");
return;
}
// Create a new coffee cup and set the temperature of
// its coffee.
CoffeeCup cup = new CoffeeCup();
cup.setTemperature(temperature);
// Create and serve a virtual customer.
VirtualPerson cust = new VirtualPerson();
VirtualCafe.serveCustomer(cust, cup);
}
}
Here, the invocation of parseInt() sits inside a
try block. Attached to the try block is
a catch clause that catches
NumberFormatException:
catch(NumberFormatException e) {
System.out.println(
"Must enter integer as first argument.");
return;
}
The lowercase character e is a reference to the
thrown (and caught) NumberFormatException object.
This reference could have been used inside the catch
clause, although in this case it isn't. (Examples of
catch clauses that use the reference are shown later
in this article.)
If the user types Harumph as the first argument
to the Example1 program, parseInt()
will throw a NumberFormatException exception and the
catch clause will catch it. The program will
print:
Must enter integer as first argument.
Although the above example had only one catch
clause, you can have many catch clauses associated
with a single try block. Here's an example:
// In Source Packet in file except/ex1/VirtualCafe.java
class VirtualCafe {
public static void serveCustomer(VirtualPerson cust,
CoffeeCup cup) {
try {
cust.drinkCoffee(cup);
System.out.println("Coffee is just right.");
}
catch (TooColdException e) {
System.out.println("Coffee is too cold.");
// Deal with an irate customer...
}
catch (TooHotException e) {
System.out.println("Coffee is too hot.");
// Deal with an irate customer...
}
}
}
If any code inside a try block throws an
exception, its catch clauses are examined in their
order of appearance in the source file. For example, if the
try block in the above example throws an exception,
the catch clause for TooColdException
will be examined first, then the catch clause for
TooHotException. During this examination process,
the first catch clause encountered that handles the
thrown object's class gets to "catch" the exception. The ordering
of catch-clause examination matters because it is
possible that multiple catch clauses of a
try block could handle the same exception.
catch clauses indicate the type of abnormal
condition they handle by the type of exception reference they
declare. In the example above, the catch clauses
declare exception type TooColdException and
TooHotException. Had a single catch
clause declared a TemperatureException, a thrown
TooColdException or TooHotException
still would have been caught, because
TemperatureException is the superclass of both these
classes. In the object-oriented way of thinking, a
TooColdException is a
TemperatureException, therefore, a
catch clause for TemperatureException
also will catch a thrown TooColdException. An
example of this is shown below:
// In Source Packet in file except/ex2/VirtualCafe.java
class VirtualCafe {
public static void serveCustomer(VirtualPerson cust,
CoffeeCup cup) {
try {
cust.drinkCoffee(cup);
System.out.println("Coffee is just right.");
}
catch (TemperatureException e) {
// This catches TooColdException, TooHotException,
// as well as TemperatureException.
System.out.println("Coffee is too cold or too hot.");
// Deal with an irate customer...
}
}
}
Multiple catch clauses could handle the same
exception because you may, for example, declare two
catch clauses, one for TooColdException
and another for TemperatureException. In this case,
however, you must place the catch clause for
TooColdException above the one for
TemperatureException, or the source file won't
compile. If a catch clause for
TemperatureException could be declared before a
catch clause for TooColdException, the
first catch clause would catch all
TooColdExceptions, leaving nothing for the second
catch clause to do. The second catch
clause would never be reached. The general rule is: subclass
catch clauses must precede superclass
catch clauses. Here's an example of both orders,
only one of which compiles:
// In Source Packet in file except/ex3/VirtualCafe.java
class VirtualCafe {
public static void serveCustomer(VirtualPerson cust,
CoffeeCup cup) {
try {
cust.drinkCoffee(cup);
System.out.println("Coffee is just right.");
}
catch (TemperatureException e) {
// This catches TooColdException, TooHotException,
// as well as TemperatureException.
System.out.println("Coffee is too cold or too hot.");
// Deal with an irate customer...
}
// THIS WON'T COMPILE, BECAUSE THIS catch clause
// WILL NEVER BE REACHED.
catch (TooColdException e) {
System.out.println("Coffee is too cold.");
}
}
}
// In Source Packet in file except/ex4/VirtualCafe.java
// This class compiles fine.
class VirtualCafe {
public static void serveCustomer(VirtualPerson cust,
CoffeeCup cup) {
try {
cust.drinkCoffee(cup);
System.out.println("Coffee is just right.");
}
catch (TooColdException e) {
System.out.println("Coffee is too cold.");
// Deal with an irate customer...
}
catch (TemperatureException e) {
// This catches TooHotException as well
// as TemperatureException.
System.out.println(
"There's temperature trouble in this coffee.");
// Deal with an irate customer...
}
}
}
Embedding information in an
exception object
When you throw an exception, you are performing a kind of
structured go-to from the place in your program where an abnormal
condition was detected to a place where it can be handled. The
Java virtual machine uses the class of the exception object you
throw to decide which catch clause, if any, should
be allowed to handle the exception. But an exception doesn't just
transfer control from one part of your program to another, it
also transmits information. Because the exception is a
full-fledged object that you can define yourself, you can embed
information about the abnormal condition in the object before you
throw it. The catch clause can then get the
information by querying the exception object directly.
The Exception class allows you to specify a
String detail message that can be retrieved by
invoking getMessage() on the exception object. When
you define an exception class of your own, you can give client
programmers the option of specifying a detail message like
this:
// In Source Packet in file except/ex5/UnusualTasteException.java
class UnusualTasteException extends Exception {
UnusualTasteException() {
}
UnusualTasteException(String msg) {
super(msg);
}
}
Given the above declaration of
UnusualTasteException, client programmers could
create an instance in one of two ways:
new UnusualTasteException()new UnusualTasteException("This coffee tastes like
tea.")A catch clause can then query the object for a
detail string, like this:
// In Source Packet in file except/ex5/VirtualCafe.java
class VirtualCafe {
public static void serveCustomer(VirtualPerson cust,
CoffeeCup cup) {
try {
cust.drinkCoffee(cup);
System.out.println("Coffee tastes just right.");
}
catch (UnusualTasteException e) {
System.out.println(
"Customer is complaining of an unusual taste.");
String s = e.getMessage();
if (s != null) {
System.out.println(s);
}
// Deal with an unhappy customer...
}
}
}
When you need to embed more information into an exception
object than you can represent with a String, you can
add data and access methods to your exception class. For example,
you could define the temperature exception classes like this:
// In Source Packet in file except/ex6/TemperatureException.java
abstract class TemperatureException extends Exception {
private int temperature; // in Celsius
public TemperatureException(int temperature) {
this.temperature = temperature;
}
public int getTemperature() {
return temperature;
}
}
// In Source Packet in file except/ex6/TooColdException.java
class TooColdException extends TemperatureException {
public TooColdException(int temperature) {
super(temperature);
}
}
// In Source Packet in file except/ex6/TooHotException.java
class TooHotException extends TemperatureException {
public TooHotException(int temperature) {
super(temperature);
}
}
Given a TemperatureException family as defined
above, catch clauses can query the exception object
to find out the precise temperature that caused the problem. The
temperature field of the exception object must be
set when the object is created, as in:
// In Source Packet in file except/ex6/VirtualPerson.java
class VirtualPerson {
private static final int tooCold = 65;
private static final int tooHot = 85;
public void drinkCoffee(CoffeeCup cup) throws
TooColdException, TooHotException {
int temperature = cup.getTemperature();
if (temperature <= tooCold) {
throw new TooColdException(temperature);
}
else if (temperature >= tooHot) {
throw new TooHotException(temperature);
}
//...
}
//...
}
Wherever the exception is caught, the catch
clause can easily determine the actual temperature of the coffee
and act accordingly, as in:
// In Source Packet in file except/ex6/VirtualCafe.java
class VirtualCafe {
public static void serveCustomer(VirtualPerson cust,
CoffeeCup cup) {
try {
cust.drinkCoffee(cup);
System.out.println("Coffee is just right.");
}
catch (TooColdException e) {
int temperature = e.getTemperature();
System.out.println("Coffee temperature is "
+ temperature + " degrees Celsius.");
if (temperature > 55 && temperature <= 65) {
System.out.println("Coffee is cooling off.");
// Add more hot coffee...
}
else if (temperature > 0 && temperature <= 55) {
System.out.println("Coffee is too cold.");
// Give customer a new cup of coffee with the
// proper temperature...
}
else if (temperature <= 0) {
System.out.println("Coffee is frozen.");
// Deal with an irate customer...
}
}
catch (TooHotException e) {
int temperature = e.getTemperature();
System.out.println("Coffee temperature is "
+ temperature + " degrees Celsius.");
if (temperature >= 85 && temperature < 100) {
System.out.println("Coffee is too hot.");
// Ask customer to let it cool a few minutes...
}
else if (temperature >= 100 && temperature < 2000) {
System.out.println(
"Both coffee and customer are steamed.");
// Deal with an irate customer...
}
else if (temperature >= 2000) {
System.out.println(
"The coffee is plasma.");
// Deal with a very irate customer...
}
}
}
}
The program could deal with the temperature problem differently depending upon the coffee's actual temperature. If the coffee is just a little cold, the program could add more hot coffee to the cup. If the coffee is so cold that the customer's lips were instantly frozen to the cup, alternative measures could be taken.
Exceptions and the method
invocation stack
Code inside a try block is in a sense surrounded by
the catch clauses associated with the
try block. When an exception is thrown, the
surrounding catch clauses are examined in inside-out
order. You can nest try blocks inside
try blocks, in effect building up more and more
layers of catch clauses that surround the code. When
a method is invoked from within a try block, the
catch clauses associated with that try
block surround the code in the invoked method as well.
If that method has try blocks and catch
clauses, they are added as inner surrounding layers. What this
means is that an exception may be thrown far up the method
invocation stack before landing in a catch clause
that can handle it.
As an example, consider the following exception classes, which are simpler versions of exceptions introduced in examples above:
// In Source Packet in file except/ex7/TemperatureException.java
class TemperatureException extends Exception {
}
// In Source Packet in file except/ex7/TooColdException.java
class TooColdException extends TemperatureException {
}
// In Source Packet in file except/ex7/TooHotException.java
class TooHotException extends TemperatureException {
}
// In Source Packet in file except/ex7/UnusualTasteException.java
class UnusualTasteException extends Exception {
}
When the drinkCoffee() method of class
VirtualPerson is invoked, it throws one of these
four exceptions, chosen at random:
// In Source Packet in file except/ex7/VirtualPerson.java
class VirtualPerson {
public void drinkCoffee(CoffeeCup cup) throws TooColdException,
TemperatureException, UnusualTasteException {
try {
int i = (int) (Math.random() * 4.0);
switch (i) {
case 0:
throw new TooHotException();
case 1:
throw new TooColdException();
case 2:
throw new UnusualTasteException();
default:
throw new TemperatureException();
}
}
catch (TooHotException e) {
System.out.println("This coffee is too hot.");
// Customer will wait until it cools to an
// acceptable temperature.
}
}
//...
}
If variable i in the drinkCoffee()
method above happens to be set to a value of 0 the switch
statement will instantiate and throw a
TooHotException. Because the switch statement itself
is enclosed within a try block that has a
catch clause for TooHotException,
execution continues at that catch clause. The
program prints out:
This coffee is too hot.
If variable i in the drinkCoffee()
method above happens to be set to the value of 1, the switch
statement will instantiate and throw a
TooColdException. When this exception is thrown, the
Java virtual machine will first check the catch
clauses of the try block that surrounds the switch
statement. In this case, however, no catch clause
matches the thrown exception.
Because the TooColdException is not caught by the
drinkCoffee() method, the Java virtual machine
throws the exception up the method invocation stack to the method
that invoked drinkCoffee(). As used here, a
method invocation stack (or call stack) is a
list of the methods that have been invoked by a thread, starting
with the first method the thread invoked and ending with the
current method. A method invocation stack shows the path of
method invocations a thread took to arrive at the current
method.
A graphical representation of the method invocation stack for
drinkCoffee() is shown in Figure 3. In this figure,
the method invocation stack is shown on the right and the
corresponding Java stack is shown on the left. The Java
stack is where methods keep their state inside the Java
virtual machine. Each method gets a stack frame (or
frame), which is pushed onto the stack when the method
is invoked and popped from the stack when the method completes.
The frame is an area in memory that contains the method's local
variables, parameters, return value, and other information needed
by the Java virtual machine to execute the method. In Figure 3,
the stack is shown growing downwards. The top of the stack is at
the bottom of the picture.
![]() Figure 3. The method invocation stack for drinkCoffee()
|
When a method completes by executing a return statement, or by
successfully executing the last statement in a method declared as
void, it is said to complete normally. The
Java virtual machine pops the returning method's stack frame, and
continues executing just after the method invocation in the
calling method. The calling method becomes the current
method and its stack frame becomes the current
frame.
When a method throws an exception that it doesn't catch itself, it is said to complete abruptly. Methods do not return a value when they complete abruptly, though they do pass along an exception object.
For example, when the drinkCoffee() method throws
a TooColdException, it completes abruptly. Because
the exception isn't caught by drinkCoffee(), the
Java virtual machine pops drinkCoffee()'s stack
frame. It then examines the next method up the invocation stack,
in this case the serveCustomer() method of
VirtualCafe, to see if it has a catch
clause prepared to handle the exception.
Here's the code for VirtualCafe:
// In Source Packet in file except/ex7/VirtualCafe.java
class VirtualCafe {
public static void serveCustomer(VirtualPerson cust,
CoffeeCup cup)throws TemperatureException,
UnusualTasteException {
try {
cust.drinkCoffee(cup);
}
catch (TooColdException e) {
System.out.println("This coffee is too cold.");
// Add more hot coffee...
}
}
}
The serveCustomer() method above does indeed
surround its invocation of drinkCoffee() with a
try block that has an attached catch
clause for TooColdException. So the exception stops
here. The Java virtual machine makes the
serveCustomer() method's stack frame current and
continues execution at the first statement inside the
catch clause. The program prints out:
This coffee is too cold.
If variable i in the drinkCoffee()
method above happens to be set to the value of 2, the switch
statement will instantiate and throw an
UnusualTasteException. When this exception is
thrown, the Java virtual machine will first check the
catch clauses of the try block that
surrounds the switch statement. In this case, no
catch clause matches the thrown exception. The
virtual machine will then pop drinkCoffee()'s stack
frame and examine the serveCustomer() method. But in
serveCustomer(), no catch clause
attached to the try block matches the thrown
exception either. The virtual machine will therefore pop
serveCustomer()'s stack frame and examine the next
method up the invocation stack: the main() method of
class Example7.
Here's the code for Example7:
// In Source Packet in file except/ex7/Example7.java
class Example7 {
public static void main(String[] args)
throws TemperatureException {
// Create a new coffee cup.
CoffeeCup cup = new CoffeeCup();
// Create and serve a virtual customer.
try {
VirtualPerson cust = new VirtualPerson();
VirtualCafe.serveCustomer(cust, cup);
}
catch (UnusualTasteException e) {
System.out.println("This coffee has an unusual taste.");
}
}
}
This main() method was farsighted enough to
surround its invocation of serveCustomer() with a
try block that includes a catch clause
for UnusualTasteException. Thus, the Java virtual
machine will make the main() method's stack frame
current and will continue execution at the first statement in the
catch clause. The program will print:
This coffee has an unusual taste.
In the UnusualTasteException case, both
drinkCoffee() and serveCoffee() methods
completed abruptly. The Java virtual machine popped two frames
from the Java stack, stopping its popping only when it reached
the main() method.
The last case in this example occurs if the variable
i in the drinkCoffee() method gets set
to a value greater than 2. In this case, the switch statement
will instantiate and throw a TemperatureException.
When this exception is thrown, the Java virtual machine will go
through its usual procedure of examining methods for
catch clauses and popping frames for methods that
can't handle the exception. The virtual machine will examine
drinkCoffee(), pop its frame, examine
serveCustomer(), pop its frame, examine
main(), and pop its frame. At this point, however,
the virtual machine has run out of frames. It can't go any
further up the method invocation stack because
main() was the first method invoked by the
thread.
Because none of the methods on the invocation stack is
prepared to handle the TemperatureException, the
exception is "uncaught." It will be handled by a default handler
and result in the death of the thread. Because this is the main
thread of the Example7 application and the
application didn't fire off any other threads that are still
running when the main thread dies, the application terminates. (A
dead thread doesn't always cause the death of its application,
only when a dying thread is the last "non-daemon" thread running
inside the application.) In most Java runtime environments, the
default handler for an uncaught exception will print out a stack
trace when a thread dies. For example, the java
program from JDK 1.1.1 prints the following when the main thread
of Example7 dies due to an uncaught
TemperatureException:
TemperatureException
at VirtualPerson.drinkCoffee(VirtualPerson.java:20)
at VirtualCafe.serveCustomer(VirtualCafe.java:9)
at Example7.main(Example7.java:12)
The throws
clause
As you may have guessed from the examples above, the Java
language requires that a method declare in a throws
clause the exceptions that it may throw. A method's
throws clause indicates to client programmers what
exceptions they may have to deal with when they invoke the
method.
For example, the drinkCoffee() method of class
VirtualPerson, shown below, declares three
exceptions in its throws clause:
TooColdException, TemperatureException,
and UnusualTasteException. These are the three
exceptions that the method throws but doesn't catch. The method
also may throw TooHotException, but this exception
doesn't appear in the throws clause because
drinkCoffee() catches and handles it internally.
Only exceptions that will cause a method to complete abruptly
should appear in its throws clause.
// In Source Packet in file except/ex7/VirtualPerson.java
class VirtualPerson {
public void drinkCoffee(CoffeeCup cup) throws TooColdException,
TemperatureException, UnusualTasteException {
try {
int i = (int) (Math.random() * 4.0);
switch (i) {
case 0:
throw new TooHotException();
case 1:
throw new TooColdException();
case 2:
throw new UnusualTasteException();
default:
throw new TemperatureException();
}
}
catch (TooHotException e) {
System.out.println("This coffee is too hot.");
// Customer will wait until it cools to an
// acceptable temperature.
}
}
//...
}
In the drinkCoffee() method above, each exception
declared in the throws clause is explicitly thrown
by the method via a throw statement. This is one of two ways a
method can complete abruptly. The other way is by invoking
another method that completes abruptly.
An example of this is VirtualCafe's
serveCustomer() method, shown below, that invokes
VirtualPerson's drinkCoffee() method.
The serveCustomer() method contains no throw
statements, but it does declare two exceptions in its
throws clause: TemperatureException and
UnusualTasteException. These are two of three
exceptions that may be thrown by drinkCoffee(),
which serveCustomer() invokes. The third exception,
TooColdException, doesn't appear in the
throws clause because serveCustomer()
catches and handles it internally. Only those exceptions that
will cause the serveCustomer() method to complete
abruptly appear in its throws clause.
// In Source Packet in file except/ex7/VirtualCafe.java
class VirtualCafe {
public static void serveCustomer(VirtualPerson cust,
CoffeeCup cup)throws TemperatureException,
UnusualTasteException {
try {
cust.drinkCoffee(cup);
}
catch (TooColdException e) {
System.out.println("This coffee is too cold.");
// Add more hot coffee...
}
}
}
Although a throws clause lists exceptions that
may cause a method to complete abruptly, the list is not
necessarily complete. Not everything that can be thrown by a
method need be put in a throws clause.
Checked vs. unchecked
exceptions
There are two kinds of exceptions in Java, checked and
unchecked, and only checked exceptions need appear in
throws clauses. The general rule is: Any checked
exceptions that may be thrown in a method must either be caught
or declared in the method's throws clause. Checked
exceptions are so called because both the Java compiler and the
Java virtual machine check to make sure this rule is obeyed.
Whether or not an exception is "checked" is determined by its
position in the hierarchy of throwable classes. Figure 4 shows
that some parts of the Throwable family tree contain
checked exceptions while other parts contain unchecked
exceptions. To create a new checked exception, you simply extend
another checked exception. All throwables that are subclasses of
Exception, but not subclasses of
RuntimeException are checked exceptions.
![]() Figure 4. Checked and unchecked throwables |
The conceptual difference between checked and unchecked
exceptions is that checked exceptions signal abnormal conditions
that you want client programmers to deal with. For instance,
because the drinkCoffee() method allocates memory
with the new operator, it could potentially complete
abruptly by throwing an OutOfMemoryError. This is
not a checked exception, because it's not a subclass of
Exception. It's a subclass of Error.
Conceptually, OutOfMemoryError isn't a checked
exception because you don't want client programmers to have to
deal directly with the fact that drinkCoffee() could
complete abruptly due to low memory.
When you place an exception in a throws clause,
it forces client programmers who invoke your method to
deal with the exception, either by catching it or by declaring it
in their own throws clause. If they don't deal with
the exception in one of these two ways, their classes won't
compile. For example, because the drinkCoffee()
method declares three exceptions in its throws
clause, the serveCustomer() method, which invokes
drinkCoffee(), has to deal with those three
exceptions. In this case, serveCustomer() catches
one exception, TooColdException, but not the other
two. If serveCustomer() hadn't declared in its
throws clause the other two exceptions,
TemperatureException and
UnusualTasteException, the VirtualCafe
class would not have compiled.
Most unchecked throwables declared in java.lang
(subclasses of Error and
RuntimeException) are problems that would be
detected by the Java virtual machine. Errors usually signal
abnormal conditions that you wouldn't want a program to handle.
Problems with linking, such as NoClassDefFoundError,
or memory, such as StackOverflowError, could happen
just about anywhere in a program. In the rare cases in which they
happen, it is usually reasonable that the thread terminate.
Although most runtime exceptions (members of the
RuntimeException family) also are thrown by the Java
virtual machine itself, they usually are more an indication of
software bugs. Problems with arrays, such as
ArrayIndexOutOfBoundsException, or passed
parameters, such as IllegalArgumentException, also
could happen just about anywhere in a program. When exceptions
like these are thrown, you'll want to fix the bugs that caused
them to be thrown. You won't, however, want to force client
programmers to wrap every invocation of a method that uses arrays
with a catch clause for
ArrayIndexOutOfBoundsException.
You can throw and catch unchecked exceptions just like checked
exceptions, but the Java Language Specification advises against
throwing errors. It is intended that errors be thrown only by the
Java runtime. You may, however, reasonably throw runtime
exceptions. You can throw a runtime exception declared in
java.lang or declare your own subclasses of
RuntimeException.
To decide whether to throw a checked exception or an unchecked
runtime exception, you must look at the abnormal condition you
are signalling. If you are throwing an exception to indicate an
improper use of your class, you are signalling a software bug.
The class of exception you throw probably should descend from
RuntimeException, which will make it unchecked.
Otherwise, if you are throwing an exception to indicate not a
software bug but an abnormal condition that client programmers
should deal with every time they use your method, your exception
should be checked.
The finally
clause
Once a Java virtual machine has begun to execute a block of code
-- the statements between two matching curly braces -- it can
exit that block in any of several ways. It could, for example,
simply execute past the closing curly brace. It could encounter a
break, continue, or return
statement that causes it to jump out of the block from somewhere
in the middle. Or, if an exception is thrown that isn't caught
inside the block, it could exit the block while searching for a
catch clause.
Given that a block can be exited in many ways, it is important
to be able to ensure that something happens upon exiting a block,
no matter how the block is exited. For example, if you open a
file in a method, you may want to ensure the file gets closed no
matter how the method completes. In Java, you express such a
desire with a finally clause.
To use a finally clause, you simply: (1) Enclose
the code that has multiple exit points in a try
block; and (2) place the code that must be executed when the
try block is exited in a finally
clause.
Here's an example:
try {
// Block of code with multiple exit points
}
finally {
// Block of code that must always be executed when the try block
// is exited, no matter how the try block is exited
}
At least one clause, either catch or
finally, must be associated with each
try block. If you have both catch
clauses and a finally clause with the same
try block, you must put the finally
clause after all the catch clauses, as in:
// In Source Packet in file except/ex8/VirtualPerson.java
class VirtualPerson {
public void drinkCoffee(CoffeeCup cup) {
try {
int i = (int) (Math.random() * 4.0);
switch (i) {
case 0:
throw new TooHotException();
case 1:
throw new TooColdException();
case 2:
throw new UnusualTasteException();
default:
System.out.println("This coffee is great!");
}
}
catch (TooHotException e) {
System.out.println("This coffee is too hot.");
}
catch (TooColdException e) {
System.out.println("This coffee is too cold.");
}
catch (UnusualTasteException e) {
System.out.println("This coffee is too strong.");
}
finally {
System.out.println("Can I please have another cup?");
}
}
//...
}
If during execution of the code within a try
block, an exception is thrown that is handled by a
catch clause associated with the try
block, the finally clause will be executed after the
catch clause. For example, if a
TooColdException exception is thrown during
execution of the try block above, the program would
print the following:
This coffee is too cold. Can I please have another cup?
If an exception is thrown that is not handled by a
catch clause associated with the try block, the
finally clause is still executed. The Java virtual
machine will execute the code of the finally clause before it
continues searching elsewhere for an appropriate catch clause.
There is no way to leave a try block without
executing the code of its finally clause.
You can do anything inside a finally clause that
you can do elsewhere, including executing break, continue, or
return statements, or throwing exceptions. Such actions inside a
finally clause, however, can have some surprising
effects. For example, consider a finally clause that
is entered because of an uncaught exception. If the
finally clause executes a return, the method would
complete normally via the return, not abruptly by throwing the
exception. The exception would have in effect been handled by the
finally clause instead of a catch
clause.
As another example, consider a finally clause
that is entered because a return true; statement was
executed inside the try block. If the
finally clause executes a return false;
statement, the method will return false.
Conclusion
Java goes to great lengths to help you deal with error
conditions. Java's exception mechanisms give you a structured way
to perform a go-to from the place where an error occurs to the
code that knows how to handle the error. These mechanisms also
enable you to force client programmers (those who use
your code by calling your methods) to deal with the possibility
of an error condition encountered by your code. But Java's
mechanisms do not force you to design your programs to take
advantage of these capabilities. In the end, if you want your
programs to handle error conditions in a structured, methodical
way, you must use the exception mechanisms correctly.
For advice on how to put the exception mechanisms described in this article to use in your programs and designs, see this month's Design Techniques column, "Designing with exceptions."
About the author
Bill Venners has been writing software professionally for 12
years. Based in Silicon Valley, he provides software consulting
and training
services under the name Artima
Software Company. Over the years he has developed software
for the consumer electronics, education, semiconductor, and life
insurance industries. He has programmed in many languages on many
platforms: assembly language on various microprocessors, C on
Unix, C++ on Windows, Java on the Web. He is author of the book:
Inside
the Java Virtual Machine, published by McGraw-Hill.
Reach Bill at bv@artima.com.
This article was first published under the name Exceptions in Java in JavaWorld, a division of Web Publishing, Inc., June 1998.
|
Sponsored Links
|