Mar 1, 2002 12:00 AM PT

Java 101: Exceptions to the programming rules, Part 1

Learn how exception handling has evolved from C to Java

Apart from the simplest programs, programs usually fail. Causes range from coding mistakes to bad user input, to operating system flaws. Regardless of the failure's cause, as a software developer, you're charged with designing programs that can either recover (where the program continues to execute after fixing the failure) or properly shut down (where the program makes every effort to save the user's data before terminating). After all, why should users pay for faulty programs that fail to save data when those programs unexpectedly terminate?

This article discusses program failure in the exception context. After defining exception, I show how C, C++, and Java handle exceptions. Knowing how to handle exceptions in C and C++ gives you an appreciation for why exception handling works the way it does in Java and lets you compare/contrast different exception-handling techniques.

What are exceptions?

Situations arise where programs fail. For example, a program tries to open a file that does not exist. Or a program tries to access a nonexistent array element while ordering a name array. In those examples, the program's proper execution flow diverges into an abnormal flow. In the first example, that divergence occurs when the program fails to open the file. In the second example, the divergence occurs when the program tries to access the nonexistent array element. Each divergence from a proper execution flow to an abnormal one is known as an exception.

Note
Additional examples of commonly occurring exceptions include attempts to write to a printer that is off, read from an unopened file, divide an integer by integer value zero, and call a method using an object reference variable containing a null reference. The first two examples illustrate resource-related exceptions because they involve resources (printer and file). The latter two examples illustrate flawed code exceptions because they arise from improperly written code.

When an exception occurs, the program must handle that exception. Failure to do so puts the program into an unstable state. Under older operating systems, like Microsoft DOS, a program might crash the operating system and reboot the computer. Under newer operating systems, like Linux and Windows XP, the operating system might forcefully terminate the program. Or, instead of terminating, the program might behave erratically. How should a program handle exceptions? The next three sections provide an overview of exception-handling features that C, C++, and Java offer to programs.

Handling exceptions in C

C and many other languages have no technique for detecting -- much less handling -- exceptions arising from flawed code. For those languages, you must carefully write code so such exceptions never occur. In contrast, a technique has always been available for detecting and handling resource-related exceptions -- error code testing.

C developers build programs from function libraries. Think of a function as a method absent from any class. Resource-related functions (such as C's fopen() -- file open -- function) return values to indicate success or failure. Failure values are known as error codes, and the use of program instructions (such as if statements) to examine return values (to see if they represent error codes) is known as error code testing. For example, C's fopen() function returns a FILE structure's address upon successfully opening a file or a NULL error code if it fails to open the file. A C program that calls fopen() typically uses an if statement to determine if that function failed, as the following C-based code fragment indicates:

FILE *fp = fopen ("myfile.dat", "rt"); // Attempt to open file myfile.dat in the read-only and text modes.
if (fp == NULL) // Detect an exception. If fopen() fails, error code testing reveals an exception.
{
    // Handle the exception.
}

Although error code testing is not difficult, it features three problems:

  • Error codes are easy to ignore: Large programs require developers to introduce error code testing in many places. Forgetting to test a function's return value in one place can affect other functions later in the code, leading to difficult-to-find bugs. For example, if a C developer chooses not to test fopen()'s return value for an error code, subsequent calls to the fread() or the fwrite() functions (to read or write to the file, respectively) could also fail -- if fopen() returns an error code.
  • Error code testing often hides the natural execution flow (in source code): When a code sequence contains many calls to resource-related functions, the code needs to test each function call's return value. While reading through the code sequence, a developer can get stuck in an examination of error code testing and miss the natural execution flow.
  • Error code testing can significantly increase a program's size: Duplication of testing and recovery code in multiple places increases program size.

If you are unfamiliar with the concepts of error codes and error code testing, and the aforementioned problems, examine Listing 1's mfc1.c -- Multiple File Copy, Version 1 -- C source code:

Listing 1. mfc1.c

// ==============================
// mfc1.c
//
// Multiple File Copy #1
//
// Created with: Borland C++ 4.52
// ==============================
#include <stdio.h>
void main (int argc, char *argv [])
{
   FILE *fpSrc, *fpDst;
   int byte, i;
   if (argc < 3)
   {
       printf ("usage: mfc1 srcfile [ srcfile ...] dstfile");
       return;
   }
   fpDst = fopen (argv [argc-1], "wb");
   for (i = 1; i < argc-1; i++)
   {
        fpSrc = fopen (argv [i], "rb");
        if (fpSrc == NULL)
        {
            printf ("Could not open %s\n", argv [i]);
            fclose (fpDst);
            return;
        }
        do
        {
           byte = fgetc (fpSrc);
           if (byte == EOF)
               break;
           if (fputc (byte, fpDst) == EOF)
           {
               printf ("Could not write byte\n");
               fclose (fpDst);
               fclose (fpSrc);
               return;
           }
        }
        while (1);
        fclose (fpSrc);
   }
   fclose (fpDst);
}

The mfc1 program copies the contents of one or more source files to a single destination file. After checking for at least three command-line arguments, mfc1 attempts to create the destination file. mfc1 next enters a loop. For each loop iteration, mfc1 attempts to open a source file and copy its contents to the destination file. If all goes well, mfc1 closes the source file and continues with the next iteration. Once mfc1 finishes copying the last source file, mfc1 exits the loop, closes the destination file, and terminates.

Note
I used Borland C++ 4.52 to create mfc1.c. In fact, I used Borland C++ 4.52 to create all of this article's C/C++ programs. If you lack access to Borland C++ 4.52, you can still compile the source code to those programs with other C++ compilers, although you might need to make minor adjustments to the source code.

mfc1 nicely illustrates the three problems with error code testing:

  • mfc1 does not check to see if fopen() succeeds in creating the destination file. That oversight is bad: if fopen() fails, each call to fputc() -- to put a character to the file -- also fails. Fortunately, you can easily resolve that problem by introducing the following source code after fpDst = fopen (argv [argc-1], "wb");:

    if (fpDst == NULL)
    {
        printf ("Could not open %s for writing\n", argv [argc-1]);
        return;
    }
    
  • Despite the small size of mfc1's source code, the source code illustrates error code testing obscuring the program's natural execution flow. The presence of the if statements -- along with the statements they execute when their expressions evaluate to true -- within the for loop contributes to the obscurity. Unfortunately, we cannot eliminate this problem. The best we can do is minimize the amount of code each if statement executes.
  • mfc1's if statements reveal the third problem -- increased code size due to the duplication of fclose (fpDst); and fclose (fpSrc); function calls.

Let's try to reduce the duplication and deal with the other two problems. After some work, we might end up with Listing 2's mfc2.c source code:

Listing 2. mfc2.c

// ==============================
// mfc2.c
//
// Multiple File Copy #2
//
// Created with: Borland C++ 4.52
// ==============================
#include <stdio.h>
void main (int argc, char *argv [])
{
   FILE *fpSrc, *fpDst;
   int byte, i;
   if (argc < 3)
   {
       printf ("usage: mfc2 srcfile [ srcfile ...] dstfile");
       return;
   }
   fpDst = fopen (argv [argc-1], "wb");
   if (fpDst == NULL)
   {
       printf ("Could not open %s for writing\n", argv [argc-1]);
       return;
   }
   for (i = 1; i < argc-1; i++)
   {
        fpSrc = fopen (argv [i], "rb");
        if (fpSrc == NULL)
        {
            printf ("Could not open %s\n", argv [i]);
            goto exit;
        }
        do
        {
           byte = fgetc (fpSrc);
           if (byte == EOF)
               break;
           if (fputc (byte, fpDst) == EOF)
           {
               printf ("Could not write byte\n");
               goto exit;
           }
        }
        while (1);
        fclose (fpSrc);
   }
exit:
   fclose (fpSrc);
   fclose (fpDst);
}

The mfc2 program fixes the problem of ignoring error codes and also reduces the obscurity and duplication problems. For small programs, like Multiple File Copy, error code testing offers a simple solution for dealing with exceptions. But for larger programs, the three problems that error code testing introduces outweigh the benefits of testing error codes. For those programs, developers need a different technique for handling exceptions. Such a technique exists within C++.

Handling exceptions in C++

The error code testing problems in C led to the development of a new exception-handling technique. That technique originated in the paper "Exception Handling for C++," (revised) written by Andrew Koenig and Bjarne Stroustrup, the father of C++. In April 1990, they presented their paper at the Usenix C++ Conference, and the throw-object/catch-object exception handling technique was born. According to that technique, when a function encounters and cannot cope with an exception, the function creates an object describing an exception. The function then throws that object; hopefully, either the function's direct or indirect caller will catch that object. If caught, the object's contents describe the exception. Code that catches the object can use the object's contents to handle the exception.

C++'s reserved word throw followed by a class name specifies an intention to throw an object. The idea is for C++ to create an object from that class and throw the object to C++'s runtime code. The runtime code searches backwards through the function call stack -- from most recently called function to least recently called function -- for a function that contains a catch clause capable of catching objects of the thrown object's class type. When found, execution passes to code found within the catch clause. For example, throw DivideByZero (); creates an object from DivideByZero and throws that object to the runtime code. That code searches the function call stack (backwards) for a function containing catch (DivideByZero). If found, execution passes to code within that clause.

To put throwing and catching objects into perspective, look at Listing 3's excdemo.cpp (C++) source code:

Listing 3. excdemo.cpp

// ==============================
// excdemo.cpp
//
// C++ exception demo
//
// Created with: Borland C++ 4.52
// ==============================
#include <iostream.h>
class Stack
{
   int size, top, *values;
   class Parent
   {
      int index, value;
      public:
      Parent (int index, int value)
      {
         this->index = index;
         this->value = value;
      }
      int getIndex ()
      {
         return index;
      }
      int getValue ()
      {
         return value;
      }
   };
   public:
   class Empty : public Parent
   {
      public:
      Empty (int index) : Parent (index, 0)
      {
      }
   };
   class Full : public Parent
   {
      public:
      Full (int index, int value) : Parent (index, value)
      {
      }
   };
   Stack (int size)
   {
      this->size = size;        // Maximum number of integer values in stack.
      top = -1;                 // Stack defaults to empty.
      values = new int [size];  // Create stack to hold size integer values.
   }
   ~Stack ()
   {
      delete [] values;         // Destroy constructor-created integer array.
   }
   int isEmpty ()
   {
      return (top == -1) ? 1 : 0;
   }
   void push (int value)
   {
      if (top >= size-1)        // When top equals size-1, the stack is full.
          throw Full (top, value); // When full, throw Stack::Full exception.
      values [++top] = value;
   }
   int pop ()
   {
      if (top < 0)              // When top less than 0, the stack is empty.
          throw Empty (top);    // When empty, throw Stack::Empty exception.
      return values [top--];
   }
};
void main ()
{
   Stack s (5); // Create a stack that holds 5 integer values (maximum).
   cout << "Stack object created. Stack holds a maximum of 5 integer values.\n";
   int values [6] = { 52, -32, 468, 345, 42, 91 };
   try
   {
       // Attempt to push 6 integer values onto the stack. The last push attempt
       // results in a Stack::Full exception.
       for (int i = 0; i < 6; i++)
       {
            cout << "Attempting to push integer value " << values [i] << ".\n";
            s.push (values [i]);
       }
   }
   catch (Stack::Full f)
   {
       cout << "Stack is full. Could not push integer value " << f.getValue ()
            << ". Top index equals " << f.getIndex () << ".\n";
   }
   try
   {
       // Attempt to pop 6 values from the stack. The last pop attempt results
       // in a Stack::Empty exception.
       for (int i = 0; i < 6; i++)
            cout << "Popping integer value " << s.pop () << ".\n";
   }
   catch (Stack::Empty e)
   {
       cout << "Stack is empty. Could not pop integer value. Top index equals "
            << e.getIndex () << ".\n";
   }
}

excdemo consists of a top-level Stack class and a main() function. Stack objects represent stack data structures. Those last-in/first-out data structures help programs keep track of things. For example, the function call stack helps C++ track the order in which functions call functions. (I explore stacks and data structures in a future article.) The main() function provides the program's entry point. When excdemo starts running, control passes to main(), which produces the following output:

Stack object created. Stack holds a maximum of 5 integer values.
Attempting to push integer value 52.
Attempting to push integer value -32.
Attempting to push integer value 468.
Attempting to push integer value 345.
Attempting to push integer value 42.
Attempting to push integer value 91.
Stack is full. Could not push integer value 91. Top index equals 4.
Popping integer value 42.
Popping integer value 345.
Popping integer value 468.
Popping integer value -32.
Popping integer value 52.
Stack is empty. Could not pop integer value. Top index equals -1.

The main() function begins by creating a Stack object capable of storing a maximum of five integer values. main() then uses cout and the overloaded << operator to output some information to the standard output device. Following that activity, main() creates an array of six integer values -- and then does something unusual: main() enters a try block.

A try block consists of statements, prefixed by keyword try, that sandwich between open/close brace characters. The statements (including function call statements) within that block can potentially throw exception objects. By placing statements in a try block, a developer tells C++ that the program plans to catch exception objects in a catch clause that associates with the try block.

A close look at the first try block reveals an attempt to push six integer values onto the stack. Because the stack has room for only five integer values, it cannot store the sixth integer value. Look at the source code for the push() member function in the Stack class. That code includes throw Full (top, value);. When Stack's internal values array runs out of room, C++ creates an object from Stack's Full member class and initializes that object to the current contents of the top and value variables. That object subsequently gets thrown, via throw, to the runtime code.

The runtime code looks for a catch clause that can catch objects of class type Stack::Full. (That syntax identifies Full as a member of Stack -- an instance inner class, as it were.) The search begins in the function that contains the try block. Immediately following the try block is a catch (Stack::Full f) clause. The runtime code notes that clause as being able to catch objects of class type Stack::Full -- and passes execution to that clause. The clause prints a message and calls various member functions on the object (referenced by f) to obtain information previously stored by push()'s throw statement. That information consists of the top index and integer value pushing at the time of the exception. Apart from displaying those values, excdemo does nothing with them.

After the catch clause handles the exception, execution leaves that clause and enters the second try block. That block attempts to pop six integer values from the stack. However, an exception occurs during the sixth pop attempt because only five integer values are on the stack. That exception results in the creation of a Stack::Empty object (in Stack's pop() member function) that is thrown to the runtime code. The runtime code finds the catch (Stack::Empty e) clause (immediately after the try block) and passes execution to that clause -- because the clause's Stack::Empty parameter type matches the thrown object's type. Appropriate exception information then prints.

Caution
Once a catch clause completes, execution does not automatically return to the try block from where the exception object was thrown -- because the try block is no longer in scope. Instead, execution continues with the first statement after the catch clause. Suppose you have code that must always execute (whether or not an exception occurs). If that code appears in the same try block as code that throws an exception, and if the "must-always-execute" code follows the code that throws an exception, the must-always-execute code will not execute should an exception be thrown. That holds true for both C++ and Java.

C++'s throw-object/catch-object technique overcomes the three problems with the error code testing technique. First, you can't ignore error codes -- because there are none. That is just as well. After all, what error code would you assign to the pop() member function, as its job is to return any integer value? Second, because you can group many statements in a try block, the natural execution flow doesn't end up obscured. In fact, a clean separation exists between that flow and exception handling. Finally, because you don't need to test return values for exception information, if statements and related recovery code don't duplicate. Arguably, the throw-object/catch-object technique's benefits are trivial in the aforementioned program. Only when you use that technique in the context of a larger program will those benefits surface.

Handling exceptions in Java

In the early 1990s, Dr. James Gosling of Sun Microsystems incorporated the C++ throw-object/catch-object exception-handling technique into Java's predecessor, Oak. That technique migrated from Oak to Java. Naturally, the C++ and Java versions of the throw-object/catch-object technique differ. For example, where C++ lets a program throw objects from any class, Java allows only java.lang.Throwable and subclass objects to be thrown. Also, the two syntaxes differ. For example, where C++ lets you specify catch (...) in source code, to catch any exception type, Java requires you to specify catch (Throwable t).

Tip
If you plan to migrate C++ code to Java, spend some time comparing and contrasting their respective throw-object/catch-object exception-handling techniques. Understanding the differences between those techniques will save you time during the migration.

To compare and contrast C++ exception handling with Java exception handling, explore the Java version of a C++ program in Listing 4. ExcDemo.java is equivalent to Listing 3's C++ excdemo source code:

Listing 4. ExcDemo.java

// ExcDemo.java
class Stack
{
   private int size, top;
   private int [] values;
   class Parent extends Exception
   {
      int index, value;
      Parent (int index, int value)
      {
         this.index = index;
         this.value = value;
      }
      int getIndex ()
      {
         return index;
      }
      int getValue ()
      {
         return value;
      }
   }
   class Empty extends Parent
   {
      Empty (int index)
      {
         super (index, 0);
      }
   }
   class Full extends Parent
   {
      Full (int index, int value)
      {
         super (index, value);
      }
   }
   Stack (int size)
   {
      this.size = size;         // Maximum number of integer values in stack.
      top = -1;                 // Stack defaults to empty.
      values = new int [size];  // Create stack to hold size integer values.
   }
   int isEmpty ()
   {
      return (top == -1) ? 1 : 0;
   }
   void push (int value) throws Full
   {
      if (top >= size-1)               // When top equals size-1, the stack is full.
          throw new Full (top, value); // When full, throw Stack::Full
                                       // exception.
      values [++top] = value;
   }
   int pop () throws Empty
   {
      if (top < 0)                  // When top less than 0, the stack is empty.
          throw new Empty (top);    // When empty, throw Stack::Empty
                                    // exception.
      return values [top--];
   }
};
class ExcDemo
{
   public static void main (String [] args)
   {
      // Create a stack that holds 5 integer values (maximum).
      Stack s = new Stack (5);
      System.out.println ("Stack object created. Stack holds a maximum" +
                          " of five integer values.");
      int values [] = { 52, -32, 468, 345, 42, 91 };
      try
      {
          // Attempt to push 6 integer values onto the stack. The last
          // push attempt results in a Stack.Full exception.
          for (int i = 0; i < 6; i++)
          {
              System.out.println ("Attempting to push integer value " +
                                  values [i] + ".");
              s.push (values [i]);
          }
      }
      catch (Stack.Full f)
      {
          System.out.println ("Stack is full. Could not push integer" +
                              " value " + f.getValue () +  ". Top" +
                              " index equals " + f.getIndex () + ".");
      }
      try
      {
          // Attempt to pop 6 values from the stack. The last pop
          // attempt results in a Stack::Empty exception.
          for (int i = 0; i < 6; i++)
               System.out.println ("Popping integer value " + s.pop () +
                                   ".");
      }
      catch (Stack.Empty e)
      {
          System.out.println ("Stack is empty. Could not pop integer" +
                              " value. Top index equals " +
                              e.getIndex () + ".");
      }
   }
}

ExcDemo's behavior and output are identical to excdemo's. But where exceptions are concerned, they differ. Those differences include:

  • In ExcDemo, Stack's Parent instance inner class extends Exception -- a subclass of Throwable. In excdemo, Parent extends no class.
  • In ExcDemo, Stack's push() and pop() method signatures include throws clauses. Those clauses identify the types of exception objects that those methods throw. excdemo has no equivalent clauses.
  • In ExcDemo, the throw statements use keyword new to create the exception objects. excdemo's throw statements include no such syntax.

Before leaving this section, consider the following difference between the C, C++, and Java exception-handling techniques: Unlike C and C++, Java allows you to detect and handle exceptions that arise from flawed code. For example, suppose you write a C++ program that contains the following code fragment:

int y = 0;
int x = 1;
cout << x / y;

The code fragment attempts to divide 1 by 0 and output the result. When the code fragment runs under an operating system like Windows 98, the operating system forcefully terminates the fragment's program. Now, suppose you incorporate the following Java code fragment into a Java program:

int y = 0;
int x = 1;
System.out.println (x / y);

Upon encountering x / y, the JVM creates an object from java.lang.ArithmeticException, initializes that object with information regarding the division by zero attempt, and searches for a catch clause that can handle that exception type. You will learn more about Java's ability to handle flawed code-based exceptions in Part 2.

Review

Dealing with exceptions is an important issue in software development. Programs must handle exceptions or subject themselves to different kinds of failure. This article introduced you to exceptions and explored how to handle them in C, C++, and Java. You learned that C programs handle exceptions via error codes and error code testing. Because the problems with error code testing prove more troublesome in larger programs, a better technique for handling exceptions arose: throwing and catching objects that describe exceptions. Although the throw-object/catch-object technique originally appeared in C++, that technique has migrated into Java. You received a taste of that technique by contrasting the C++ and Java versions of programs that handle exceptions with the throw-object/catch-object technique.

I encourage you to email me with any questions you might have involving either this or any previous article's material. (Please keep such questions relevant to material discussed in this column's articles.) Your questions and my answers will appear in the relevant study guides.

Part 2 will conclude our investigation of exceptional programming. It will focus on Java's features for designing new exception classes, throwing exceptions, catching exceptions, and performing cleanup operations on your code -- whether or not that code throws an exception object.

Jeff Friesen has been involved with computers for the past 20 years. He holds a degree in computer science and has worked with many computer languages. Jeff has also taught introductory Java programming at the college level. In addition to writing for JavaWorld, he has written his own Java book for beginners -- Java 2 By Example, Second Edition (Que Publishing, 2001; ISBN: 0789725932) -- and helped write Special Edition Using Java 2 Platform (Que Publishing, 2001; ISBN: 0789724685). Jeff goes by the nickname Java Jeff (or JavaJeff). To see what he's working on, check out his Website at http://www.javajeff.com.

Learn more about this topic