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... MegaPaintRemember 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 ]