Section 9.4
Programming with Exceptions
EXCEPTIONS CAN BE USED to help write robust
programs. They provide an organized and structured approach to robustness.
Without exceptions, a program can become cluttered with if statements
that test for various possible error conditions. With exceptions, it
becomes possible to write a clean implementation of an algorithm that will
handle all the normal cases. The exceptional cases can be handled elsewhere,
in a catch clause of a try statement.
Writing New Exception Classes
When a program encounters an exceptional condition and has no
way of handling it immediately, the program can throw an exception.
In some cases, it makes sense to throw an exception belonging
to one of Java's predefined classes, such as IllegalArgumentException
or IOException. However, if there is no standard class
that adequately represents the exceptional condition, the programmer
can define a new exception class. The new class must extend the
standard class Throwable or one of its subclasses. In general,
the new class will extend RuntimeException (or one of its
subclasses) if the programmer does not want to require
mandatory exception handling. To create a new exception class that does
require mandatory handling, the programmer can extend one of the
other subclasses of Exception or can extend Exception
itself.
Here, for example, is a class that extends Exception, and therefore
requires mandatory exception handling when it is used:
public class ParseError extends Exception {
public ParseError(String message) {
// Constructor. Create a ParseError object containing
// the given message as its error message.
super(message);
}
}
The class contains only a constructor that makes it possible to
create a ParseError object containing a given error message.
(The statement "super(message)" calls a constructor in
the superclass, Exception. See Section 5.5.)
Of course the class inherits the getMessage() and printStackTrace()
routines from its superclass. If e refers to an object of type
ParseError, then the function call e.getMessage() will
retrieve the error message that was specified in the constructor.
But the main point of the ParseError class is simply to exist.
When an object of type ParseError is thrown, it indicates that
a certain type of error has occurred. (Parsing,
by the way, refers to figuring out the meaning of a string. A ParseError
would indicate, presumably, that some string being processed by the program
does not have the expected form.)
A throw statement can be used in a program to throw an error of
type ParseError. The constructor for the ParseError object
must specify an error message. For example:
throw new ParseError("Encountered an illegal negative number.");
or
throw new ParseError("The word '" + word
+ "' is not a valid file name.");
If the throw statement does not occur in a try
statement that catches the error, then the subroutine that contains
the throw statement must declare that it can throw
a ParseError. It does this by adding the clause "throws ParseError"
to the subroutine heading. For example,
void getUserData() throws ParseError {
. . .
}
This would not be required if ParseError were defined as a subclass
of RuntimeException instead of Exception, since in that
case exception handling for ParseErrors would not be mandatory.
A routine that wants to handle ParseErrors can use a try
statement with a catch clause that catches ParseErrors.
For example:
try {
getUserData();
processUserData();
}
catch (ParseError pe) {
. . . // Handle the error
}
Note that since ParseError is a subclass of Exception,
a catch clause of the form "catch (Exception e)" would also
catch ParseErrors, along with any other object of type Exception.
Sometimes, it's useful to store extra data in an exception object. For
example,
class ShipDestroyed extends RuntimeException {
Ship ship; // Which ship was destroyed.
int where_x, where_y; // Location where ship was destroyed.
ShipDestroyed(String message, Ship s, int x, int y) {
// Constructor: Create a ShipDestroyed object
// carrying an error message and the information
// that the ship s was destroyed at location (x,y)
// on the screen.
super(message);
ship = s;
where_x = x;
where_y = y;
}
}
Here, a ShipDestroyed object contains an error message and
some information about a ship that was destroyed. This could be used,
for example, in a statement:
if ( userShip.isHit() )
throw new ShipDestroyed("You've been hit!", userShip, xPos, yPos);
Note that the condition represented by a ShipDestroyed object might
not even be considered an error. It could be just an expected interruption
to the normal flow of a game. Exceptions can sometimes be used to handle such interruptions
neatly.
Exceptions in Subroutines and Classes
The ability to throw exceptions is particularly useful in writing
general-purpose subroutines and classes that are meant to be used in
more than one program. In this case, the person writing the subroutine
or class often has no reasonable way of handling the error, since that
person has no way of knowing exactly how the subroutine or class will
be used. In such
circumstances, a novice programmer is often tempted to print an error
message and forge ahead, but this is almost never satisfactory since
it can lead to unpredictable results down the line. Printing an
error message and terminating the program is almost as bad, since
it gives the program no chance to handle the error.
The program that
calls the subroutine or uses the class needs to know that the error
has occurred. In languages that do not support exceptions, the only
alternative is to return some special value or to set the value
of some variable to indicate that an error has occurred.
For example, the readMeasurement() function in
Section 2 returns the value -1
if the user's input is illegal. However, this only works if the
main program bothers to test the return value. And in this case,
using -1 as a signal that an error has occurred makes it
impossible to allow negative measurements. Exceptions are
a cleaner way for a subroutine to react when it encounters an error.
It is easy to modify the readMeasurement() subroutine
to use exceptions instead of a special return value to signal an
error. My modified subroutine throws a ParseError when
the user's input is illegal, where ParseError is the
subclass of Exception that was defined earlier in this
section. (Arguably, it might be more reasonable to avoid defining
a new class by using the standard exception class IllegalArgumentException
instead.) The changes from the original version are shown
in red:
static double readMeasurement() throws ParseError {
// Reads the user's input measurement from one line of input.
// Precondition: The input line is not empty.
// Postcondition: The measurement is converted to inches and
// returned. However, if the input is not legal,
// a ParseError is thrown.
// Note: The end-of-line is NOT read by this routine.
double inches; // Total number of inches in user's measurement.
double measurement; // One measurement,
// such as the 12 in "12 miles."
String units; // The units specified for the measurement,
// such as "miles."
char ch; // Used to peek at next character in the user's input.
inches = 0; // No inches have yet been read.
skipBlanks();
ch = TextIO.peek();
/* As long as there is more input on the line, read a measurement and
add the equivalent number of inches to the variable, inches. If an
error is detected during the loop, end the subroutine immediately
by throwing a ParseError. */
while (ch != '\n') {
/* Get the next measurement and the units. Before reading
anything, make sure that a legal value is there to read. */
if ( ! Character.isDigit(ch) ) {
throw new ParseError(
"Expected to find a number, but found " + ch);
}
measurement = TextIO.getDouble();
skipBlanks();
if (TextIO.peek() == '\n') {
throw new ParseError(
"Missing unit of measure at end of line.");
}
units = TextIO.getWord();
units = units.toLowerCase();
/* Convert the measurement to inches and add it to the total. */
if (units.equals("inch")
|| units.equals("inches") || units.equals("in")) {
inches += measurement;
}
else if (units.equals("foot")
|| units.equals("feet") || units.equals("ft")) {
inches += measurement * 12;
}
else if (units.equals("yard")
|| units.equals("yards") || units.equals("yd")) {
inches += measurement * 36;
}
else if (units.equals("mile")
|| units.equals("miles") || units.equals("mi")) {
inches += measurement * 12 * 5280;
}
else {
throw new ParseError("\"" + units
+ "\" is not a legal unit of measure.");
}
/* Look ahead to see whether the next thing on the line is
the end-of-line. */
skipBlanks();
ch = TextIO.peek();
} // end while
return inches;
} // end readMeasurement()
In the main program, this subroutine is called in a try
statement of the form
try {
inches = readMeasurement();
}
catch (ParseError e) {
. . . // Handle the error.
}
The complete program can be found in the file
LengthConverter3.java.
From the user's point of view, this program has exactly the same
behavior as the program LengthConverter2 from Section 2,
so I will not include an applet version of the program here.
Internally, however, the programs are different, since
LengthConverter3 uses exception-handling.
Assertions
Recall that a precondition is a condition that must be true
at a certain point in a program, for the execution of the program
to continue correctly from that point. In the case where there is
a chance that the precondition might not be satisfied, it's a
good idea to insert an if statement to test it.
But then the question arises, What should be done if the precondition
does not hold? One option is to throw an exception. This
will terminate the program, unless the exception is caught and handled elsewhere
in the program.
The programming languages C and C++ have always had a facility for
adding what are called assertions to a program. These assertions
take the form "assert(condition)",
where condition is a boolean-valued expression.
This condition expresses a precondition that must hold at that point
in the program. When the computer encounters an assertion during the
execution of the program, it evaluates the condition. If the condition is
false, the program is terminated. Otherwise, the program continues
normally. Assertions are not available in Java 1.3,
but an assertion facility similar to the C/C++ version
has been added to the language as of Java 1.4.
Even in versions of Java before 1.4, you can do something similar to assertions:
You can test the condition using an if statement and throw an
exception if the condition does not hold.
if (condition == false)
throw new IllegalArgumentException("Assertion Failed.");
Of course, you could use a better error message. And it would
be better style to define a new exception class instead of
using the standard class IllegalArgumentException.
This sort of test is most useful during testing and debugging
of the program. Once you are sure that the program is correct,
the test in the if statement might be seen as a waste of the computer's
time. One advantage of assertions
in C and C++ is that they can be "turned off" at compile time.
That is, if the program is compiled in one way, then the assertions are included
in the compiled code. If the program is compiled in another way,
the assertions are not included. During debugging, the first
type of compilation is used. The release version of the program
is compiled with assertions turned off. The release version
will be more efficient, because the computer won't have to
evaluate all the assertions. The nice part is that the source
code doesn't have to be modified to produce the release version.
The assertion facility in Java 1.4 and later takes all this
into account. A new assert statement is introduced into
the language that has the syntax
assert condition : error-message ;
The condition in this statement is a boolean-valued expression.
The idea is that this condition is something that is supposed to be true at that point
in the program, if the program is correct.
The error-message is generally a string (though in fact it can
be an expression of any type). When an assert statement is executed,
the expression in the statement is evaluated.
If the condition is true, the assertion has no effect and the program proceeds
with the next statement. If the condition is false, then an error of type
AssertionError is thrown, and this will cause the program to crash.
The error-message is passed to the
AssertionError object and becomes part of the error message that is printed
when the program is terminated. (Of course, it's possible to catch the
AssertionError to stop the program from crashing, but the whole point of
an assertion is to make the program crash if it has gotten into
some state where a necessary condition is false.)
By default, however, assert statements are not executed. Remember that
assertions should only be executed during testing and debugging, so there has
to be some way to turn them on and off. In C/C++, this is done at
compile time; in Java, it is done at run time. When you run a program
in the ordinary way using the java command, assertions in the
program are ignored. To have an effect, they must be enabled.
This is done by adding an option to the java command.
The form of the option is "-enableassertions:class-name"
to enable all the assertions in a specified class or
"-enableassertions:package-name..."
to enable all the assertions in a package and in its sub-packages.
To enable assertions in the "default package" (that is, classes that
are not specified to belong to a package, like almost all the classes in
this book), use "-enableassertions:...". You can abbreviate
"-enableassertions" as "-ea", and you can use this option several times
in the same command. For example, to run a Java program named "MegaPaint"
with assertions enabled for the packages named "paintutils" and "drawing", you would
use the command:
java -ea:paintutils... -ea:drawing... MegaPaint
Remember that you would use the "-ea" options during development of the
program, but your customers would not have to use them when they run
your program.
End of Chapter 9
[ Next Chapter
| Previous Section
| Chapter Index
| Main Index
]
|