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 a facility for adding 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 of this form are not available in Java, but something similar can be done with exceptions. The Java equivalent of assert(condition) is:

           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.

Assertions are most useful during testing and debugging. Once you release your program, you don't really want it to crash. Still, many programs are released with a main program that says, essentially

    try {
        .
        .  // Run the program.
        .
    }
    catch (Exception e) {
       System.out.println("An unexpected internal error has occurred.");
       System.out.println("Please submit a bug report to the programmer.");
       System.out.println("Details of the error:"):
       e.printStackTrace();
    }

If a program contains a large number of assertions, they might slow the program down significantly. One advantage of assertions in C and C++ is that they can be "turned off." 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.

There is something similar that might work in Java, depending on how smart your compiler is. Suppose that you define a constant

       static final boolean DEBUG = true;

and express your assertions as:

           if (DEBUG == true && condition == false)
               throw new IllegalArgumentException("Assertion Failed.");

Since DEBUG is true, the value of "DEBUG == true && condition == false" is the same as the value of condition, so this if statement still works as a test of the precondition. Now suppose you are finished debugging. Before you compile the release version of the program, change the definition of DEBUG to

       static final boolean DEBUG = false;

Now, the value of "DEBUG == true && condition == false" has to be false, and a smart compiler can tell this at compilation time. Given that the condition in the if statement is known to be false, a smart compiler will not even bother to include the if statement in the compiled code, since it would not be executed in any case. So, the compiled code for the release version will be shorter and more efficient than the debugging version. And you only had to change one line in the source code!



End of Chapter 9

[ Next Chapter | Previous Section | Chapter Index | Main Index ]