15-100 Lecture 8 (Tuesday, June 3, 2008)

Files From Today's Class

Exceptional Control Flow

Let's think about the normal process involved when two object communicate. One object sends another object a message. That occurs by a method invocation. Then, after exhibiting some behavior, the receiving object replies. It does that by returning from the method call, and possibly by returning a value.

But, what happens when something out of the ordinary happens? For example, what happens if the method encounters an error and can't perform the requested actions? In C or Pascal, this is typically handled by using some special return value.

For example, in C, the convention is that all functions return "int" values. A return value of 0 indicates that the function completed successfully. Each negative return value generally indicates a different error. And, each positive return value generally indicates a non-error condition that prevented the function from completing. Since the return values are used to indicated the return type, values that are returned are generally managed as parameter's using C's "pass by address" mechanism.

But, this type of undocumented hackery is not necessary in C++ or Java. These languages support an "exception mechanism" that can be used to handle errors and other exception conditions -- both within a message and to communicate unhandled errors up the call chain.

As you'll learn today, Java's exception mechanism is far superior to the approach used by C, Pascal, and others. It can be used to remove error handling from the body of code, making the algorithmic component much cleaner. It is also much more self-documenting, because each type of error is represented by a meaningfully named class, not by a number. And, since it provides a different mechanism for returning in the exceptional case than the normal case, return values can be just that. All in all, it makes code more logical and more readable.

Representing an Error (Or Other Exceptional Condition)

In Java, errors or other exceptional conditions are represented with instances of the Exception class. An Exception is a reasonably simple class. For our purposes it has a two constructors, one of which take as String message, and one of which is the default constructor, which takes no parameters.

When an exception event, a.k.a, exception, occurs, the code that detects it creates a new Exception to represent the problem. When calling the constructor, it generally sets detailed information about the situation in plain-language, by passing it as the "message" to the constructor. When an Exception is converted to a String using toString(), it is this message that becomes part of the returned String.

Types of Exceptions

As we discussed moments ago, when errors or conditions were returned from C or Pascal functions, this return often took the form of a negative return value, with a different return value for each condition. The calling function would simply use a collection of if statements, or a switch/case statement to demultiplex the error conditions and take appropriate action to sort out the return value and take appropriate action. But, so far, in Java, we've only discussed one type of Exception. How are different conditions represented?

Java represents different types of Exceptions using inheritence. In Java, the generic Exception class can be extended to form different types of Exceptions. In fact, even the derived types are often extended to create even more specific types of exceptions.

Just to show an example, here is a very small piece of the Exception inheritence tree:

Creating Your Own Exceptions

You can create your own Exceptions, just by extending the Exception class. Let's consider a situation where we ask the user to enter a number between one and ten, inclusive. If the user enters a number outside of this range, we might want to throw a OutOfRangeException.

  class OutOfRangeException extends Exception {
  
    public OutOfRangeException (String message) {
      super (message);
    }
  }
  

You probably haven't seen it before, but "super()" invokes the constructor of the super, a.k.a., parent class. In other words, the constructor for OutOfRangeException will take a message as a String parameter and pass it to the constructor of the Exception class. Our function is now an Exception and handles a message exactly as does the generic type -- but, since we have a new type of extension, we will be able to tell what type of situation is present.

Throwing Exceptions

When we "throw" an exception, we activate Java's exception handling facility. Throwing an exception is easy. We simply create the exception and throw it, as shown below:

  public static void verifyRangeInclusive (int lower, int upper, int candidate)
      throws OutOfRangeException {

    if ( (candidate < lower) || (candidate > upper))
      throw new OutOfRangeException ("" + candidate + 
                                     " is outside of the range " +
                                     lower + " - " + upper + ", inclusive.")
  }
  

Notice the clause, "throws OutOfRangeException". Unless you specifically use an exception that is of an "unreported" type, when a method can throw an exception, it must be documented like this. If the method can throw more than one, they are just listed, separated using commas.

Try and Catch

So we've learned how to throw new exceptions, but if we just throw the exception, our program is going to crash because the exception isn't handled. To be more precise, the exception will be thrown down the call stack until it is thrown out of main(), at whcih time the exception will be printed and the program will end.

So how do we go about actually handling the exception so that things don't have to die? This is a two step process. First we identify the code that is at risk for an exception using a "try block". Second, we "catch" or handle any exceptions that happened to occur within the try block.

Each try block needs to be followed by at least one "catch". Each "catch" catches a particular type of exception and is activated only in the event that that particular type of exception arises. As a result, we might need several different catch blocks to handle different types of exceptions.

A catch block is activated by an exception if (a) the exception is an exact match or, (b) the exception is derived, directly or indirectly, from the one associated with the catch block. For example, if the catch block catches Exceptions, it will catch any type of Exception, such as a NumberFormatException, or an OutOfRangeException. Remember, this relationship is established by the "extends" clause in the exception's definition.

Once we catch an exception, it is caught. It will not continue to travel. So, if we catch an exception in one catch block, it will not be subsequently caught by another catch block, either locally, or further along the call chain. If we do want to allow an exception to be caught by a catch block further down the call chain, we can "rethrow" it, by throwing the exception within the catch block. Since the catch block, itself, is not within the try block, it will be thrown further down the call chain, not further down the list of catches associated with the same try block.

The example below catches both the InputMismatchException thrown by the nextInt() method if the user enters a non-integer. It also catches the OutOfBoundsException thrown by the verifyRange() method if the user enters a number outside of the prescribed range.

    try {
      int intNumber = keyboard.nextInt();
      verifyRangeInclusive (1, 10, intNumber);
      System.out.println ("The number was: " + intNumber);
    }
    catch (InputMismatchException ime) {
      System.out.println ("Not an int number");
      ime.printStackTrace(); // Print the stack trace (see discussion under exception propogation)
    }
    catch (OutOfRangeException oore) {
      System.out.println (oore.getMessage()); // Print the mesage
    }
    catch (Exception e) {
      System.out.println ("Unexpected exception: " + e);
      throw e; // Rethrow
    }
  

Exception Propogation

We'll talk more about how methods are called in when we talk about "recursion". But, for now let me observe that a compiler needs to orchestrate not only the invocation of methods, but also their return. In other words, when a method returns, it needs to return to the same place that it was before it was called. This is true for the first method that is called -- and for each subsequent method that is called. It doesn't matter how many are called, each one needs to return to exactly where it left off.

The upshot is that the compiler keeps a list, known as the call stack of the methods that are called leading to the current method. When an exception is thrown, it travels along the call stack, from one method to its caller, until it is handled. If it is not handled by the time it leaves main, the program ends and prints the list of methods that it travelled along the stack.

The example below blows up because there is no string associated with the variable "s". A NullPointerException is thrown when we try to call the toUpperCase() method via a null reference. Notice the call stack that is printed when it runs:

  import java.io.*;
  import java.util.*;

  class ExceptionStack {

    public static void someOtherMethod(String s) {
      s = s.toUpperCase(); // Note "s" is null, so BANG NullPointerException
      System.out.println (s);
    }

    public static void main (String[] args) {
      String s = null;
      someOtherMethod (s);    
    }
  
  }
  

Below is the output generated as this program dies. Notice that is shows the path from main into someOtherMethod(). This is invaluable debugging information.

  % java ExceptionStack
  Exception in thread "main" java.lang.NullPointerException
        at ExceptionStack.someOtherMethod(ExceptionStack.java:8)
        at ExceptionStack.main(ExceptionStack.java:15)
  

Exceptions As Inner Classes

Today, we created the OutOfRangeException as an independent class, in its own file. We could also have nested this exception class within the ExceptionExample class. If the exception were qualified as "private", then it would only be useful within the class -- meaning that none of the public methods could throw the exception. But, if it were declared as "public", it could be thrown. Within the class, it would be known as OutOfRangeException. But, outside of the class, it would be known as ExceptionExample.OutOfRangeException.

Notice how, in this case, the .-scope operator identifies the exception as being nested. This, among other things, prevents a name-space conflict if more than one class happens to define an exception with the same name.

Similarly, by nesting an exception and making it private, we can hide it completely. It doesn't pollute the name space outside of the class. It can't be used outside of the class. It is basicalyl invisible -- except that it can be used internally.

We'll see examples of these next class.