Java 101: Exceptions to the programming rules, Part 1

Learn how exception handling has evolved from C to Java

1 2 3 Page 2
Page 2 of 3
// ==============================
// 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.
1 2 3 Page 2
Page 2 of 3