Site hosted by Angelfire.com: Build your free website today!

Exception By Design (from Performance Computing - November 1998)

Previously, we discussed the use of exceptions in Java (UNIX Review, June 1997, p. 69). Exceptions indicate that an error has occurred, and Java supports exceptions by separating the code handling the errors from the main-line code. Part of this mechanism is centered around the method in which the error occurs by throwing an instance of an Exception class.

Exception classes can be defined to reflect different kinds of errors. The code that handles the exception is determined by the first catch clause in the chain of calling methods that matches the type of the Exception object thrown. For example, if we call a method that could generate two different types of Exception objects, our code might look something like:

try {
  int retVal = someMethod(someParam);
  } catch (Type1Exception e){
    //code that handles Type1Exception errors
  } catch (Type2Exception e){
      //code that handles Type2Exception errors
  }

This is about as far into exceptions as most Java programmers go. But how we structure our Exception types is as important as how we structure other types in a system.

All Exception classes are derived from the class java.lang.Exception, which in turn comes from the class java.lang.Throwable. These classes define a number of useful methods for Exception objects, including methods that allow the code catching an Exception object to print out a stack trace or get an error message. The base Exception class also defines two constructors, one of which takes no arguments and the other which takes a String argument. If the second of these is used, the String argument gets returned as the value of the getMessage() method on the object, and is included in the stack trace provided by the Exception object.

Most Java programs (at least ones that I've seen) treat Exception classes as handy ways of labeling errors and, perhaps, displaying said error messages. These uses are not, in themselves, a bad thing. Any way of giving more information when something goes wrong is a plus. But these classes can have more value than that. To see how, let's look at a simple example.

Suppose we want to construct a class that implements a simple first-in, first-out queue. The queue will have a fixed length determined when it is first created. We will store only values of type int (a more general version stores values of type Object, but that isn't the point of this discussion). The basic code for such a FIFO would look like:

package FifoExample;

public class Fifo{
    private int[] values;
    private int bottom, top;
    private boolean empty = true;

    public Fifo(int size){
        values = new int[size];
        bottom = top = size - 1;
    }

    public Fifo(){
        values = new int[10];
        bottom = top = 9;
    }

    public int get() throws FifoEmptyException{
        if (empty)
            throw (new FifoEmptyException("Fifo is
              empty"));
        int value = values[bottom];
        bottom = decrement(bottom);
        empty = (bottom == top);
        return value;
    }

    public void put(int value)throws FifoFullException{
        if (!empty && (top == bottom))
            throw (new FifoFullException("Fifo is 
              full"));
        values[top] = value;
        top = decrement(top);
        empty = false;
        return;
    }

    private int decrement(int value){
        value-;
        if (value < 0)
            value = values.length - 1;
        return value;
    }

    public static void main (String[] args){
        Fifo store = new Fifo();

        try {
            for (int i = 0; i<store.values.length; 
              i++){
                store.put(i);
            }
            for (int i = 0; i < store.values.length; 
              i++){
                System.out.println("value " + i + " is 
                  " + store.get());
            }

            for (int i = 0; i <= store.values.length; 
              i++)
                store.put(i);
        } catch (FifoFullException e){
            e.printStackTrace();
            System.exit(1);
        } catch (FifoEmptyException e){
            e.printStackTrace();
            System.exit(1);
        }
    }                        
}

The Fifo class is pretty basic, consisting of two constructors (one to specify how large the Fifo object should be, and the other to make one of a default size), a put() method that enables putting something in the Fifo, and a get() method that lets us take something out.

What is of interest are the two exceptions we have used in this class. If the Fifo object is empty, the call to get() throws a FifoEmptyException, and if the Fifo object is full, a call to the put() method throws a FifoFullException.

If we define these exceptions in the usual way, they look something like:

package FifoExample;

public class FifoEmptyException extends Exception{
    public FifoEmptyException(){
        super();
    }

    public FifoEmptyException(String message){
        super(message);
    }
}

package FifoExample;

public class FifoFullException extends Exception{
    public FifoFullException(){
        super();
    }

    public FifoFullException(String message){
        super(message);
    }
}

Our FIFO exceptions are part of the FifoExample package, but both simply extend the base java.lang.Exception. We could label either to describe the error (although we don't), but beyond the description, there is not much to either exception.

Using exceptions essentially as error labels is progress over the C-style approach to error handling, in which "impossible" return values are used to signal an error. The exceptions we have defined let us use the type system to help distinguish between different kinds of errors, and with the Java exception mechanism, we can separate the code used to deal with the errors from our main-line code. Both help make our code more readable and our programs more reliable. But spending a little time on designing the exception classes in our software can improve it even more.

Notice that in our test code for the Fifo class (contained in the main() method), we handle the two kinds of exception we have defined in the same way. When either exception is caught, we simply print a stack trace and exit the program. If we want to treat both exceptions this way, there is good reason to suspect that our Fifo class users also might want to treat them the same way.

We do this by writing our catch clauses to catch the common superclass of the two exceptions. But in the way we have declared our exceptions, the superclass in common is Exception. Catching this would catch every user-level exception, not just the ones relating to the Fifo class.

A better approach is defining a common superclass between the most general Exception class and the particular exceptions thrown in our Fifo class. Such a class could look like:

package FifoExample;

public class FifoException extends Exception{
    public FifoException(){
        super();
    }

    public FifoException(String message){
        super(message);
    }
}

We then define both FifoFullException and FifoEmptyException to extend FifoException rather than Exception. Our test code then can be simplified (always a good thing) to have a single catch clause that looks like

  catch (FifoException e){
    e.printStackTrace();
    System.exit(1);
}

which works the same as the two catch clauses in our previous example.

In general, it is a good idea to think about the relations among the exception classes in a package as carefully as we think about the relations between the other classes in the package. It often makes sense to have a single base class for all the exceptions in some particular package and then to derive more specific exceptions off that base, rather than simply extending the language-basic class of java.lang.Exception.

But if we will treat FifoFullException objects and FifoEmptyException objects the same, why have exception classes beyond the basic FifoException class? We can find out what happened by looking at the stack trace, and if all that will happen is that we print out the trace and exit, then why not simplify the number of types by having a single exception for both Fifo errors?

There are times when this approach is the right one. Not all kinds of errors need to have different classes of Exception objects to flag them. But in this case, there is good reason to have a different kind of Exception object generated by the different errors. While our test code might not want to distinguish between the two kinds of errors, real code that used our Fifo class might want to do different things in the face of an empty queue and a full queue.

In particular, when faced with a full queue, a program using our Fifo class might want to create a new Fifo object with larger capacity than the existing one (and then copy over the contents of the existing queue to the new one). But to do this, the program needs to know the size of the full queue. This is easy in our test case (because the test code is part of the Fifo object, it can make use of the private fields to find the length of the queue). Other users cannot get this information, since the information is a private state in the Fifo object.

We could add a method to the Fifo class that lets anyone get the size of the queue. But a better alternative is to add this information so that it is available only when needed, and allow it to be retrieved directly from the FifoFullException object.

Exception classes, after all, are real classes. They can extend their base classes by adding extra state and method calls. So when designing an exception class, we should think not only of that class as a way to signal an error, but also as the container for the information needed to recover from the error.

To provide this information, we need to redefine the FifoFullException to look something like:

package FifoExample;

public class FifoFullException extends FifoException{
    private int size;
    
    private FifoFullException(){
        super();
    }

    private FifoFullException(String message){
        super(message);
    }

    public FifoFullException(String message, int size){
        super(message);
        this.size = size;
    }

    public int getCurrentSize(){
        return size;
    }
}

We have added the state that permits a response to the error we get from trying to add a value to a full queue. We have included a constructor that allows the size of the full queue to be handed into the exception object during creation. We also have made the other two constructors private, ensuring that no FifoFullException object will be created without knowing the size of the current, full queue. Finally, we put in a method that will return the size of the queue.

A user of a Fifo object now can get the information needed to recover from trying to put something into a full queue. The FifoFullException object returned includes the size of the full queue, so a new queue can be created with the best size increase for the user. The contents of the old queue can be drained and placed in the new one, and new values can be added to the newly created larger queue.

All this occurred because we thought about the information needed for recovering from an error, and made sure this information was included in the exception generated by the error. We even could have helped the client more, by including a method in the FifoFullException that would repair everything.

How to help the user is open to a number of design decisions. What is important is thinking about what response might be taken, and making sure the information is there to help in that response.