Chapter 7

Exceptions


CONTENTS


In this chapter you'll learn how to use exceptions to implement error-handling capabilities in your Java programs. You'll learn how to declare exceptions and identify methods that use them. You'll also learn how to throw exceptions in response to error conditions and how to catch exceptions in support of error processing. When you finish this chapter, you'll be able to use exceptions to handle all sorts of errors in your programs.

Eliminating Software Errors

Programs are reliable, in part, because they are able to cope with errors and exceptions that occur during their execution. The development of reliable, error-tolerant software is a multiphase effort that spans program design, coding, compilation, loading, and execution.

The most serious errors are those that are designed into a program. Many design errors can be eliminated by following a sound development approach, using modern software engineering methods, and making a firm commitment to software quality. The use of an object-oriented approach to software development helps to simplify software design, reduce errors, and promote software reliability.

Programming errors initially occur when a software design is translated into source code. Program verification, validation, analysis, and test activities help to eliminate design and programming errors. The implementation of coding standards and code walkthroughs also reduces the likelihood of undetected programming errors.

The Java language eliminates whole classes of errors that result from the use of dangerous programming constructs such as pointers and automatic type conversions. The simplicity and familiarity of the language also reduce the occurrence of programming errors.

The Java compiler and runtime environment help to keep errors from being introduced into executable Java code. The compiler performs extensive type checking to ensure that objects are correctly referenced and updated and that methods are properly invoked. The runtime system duplicates compile-time checks and implements additional checks to verify that executable code follows established rules for program correctness.

With all the error checking that takes place before a program is executed, you might think that it would be unlikely that errors could still creep into a program. They can and always do, in accordance with Murphy's Law. Runtime error and exception handling is used to identify error conditions and perform processing that minimizes their impact.

Error Processing and Exceptions

Java provides superior support for runtime error and exception handling, allowing programs to check for anomalous conditions and respond to them with minimal impact on the normal flow of program execution. This allows error- and exception-handling code to be added easily to existing methods.

Exceptions are generated by the Java runtime system in response to errors that are detected when classes are loaded and their methods are executed. The runtime system is said to throw these runtime exceptions. Runtime exceptions are objects of the class java.lang.RuntimeException or of its subclasses.

Exceptions may also be thrown directly by Java code using the throw statement. These exceptions are thrown when code detects a condition that could potentially lead to a program malfunction. The exceptions thrown by user programs are generally not objects of a subclass of RuntimeException. These non-runtime exceptions are referred to as program exceptions.

Note
It is possible for user programs to throw runtime exceptions, but it is almost always a bad programming practice.

Both program and runtime exceptions must be caught in order for them to be processed by exception-handling code. If a thrown exception is not caught, its thread of execution is terminated and an error message is displayed on the Java console window.

The approach used by Java to catch and handle exceptions is to surround blocks of statements for which exception processing is to be performed with a try statement. The try statement contains a catch clause that identifies what processing is to be performed for different types of exceptions. When an exception occurs, the Java runtime system matches the exception to the appropriate catch clause. The catch clause then handles the exception in an appropriatemanner.

Throwing Exceptions

Exceptions are thrown using the throw statement. Its syntax is as follows:

throw Expression;

Expression must evaluate to an object that is an instance of a subclass of the java.lang.Exception class. The Exception class is defined in the Java API. When an exception is thrown, execution does not continue after the throw statement. Instead, it continues with any code that catches the exception. If an exception is not caught, the current thread of execution is terminated and an error is displayed on the console window.

For example, the following statement will throw an exception, using an object of class ExampleException:

throw new ExampleException();

The new operator is invoked with the ExampleException() constructor to allocate and initialize an object of class ExampleException. This object is then thrown by the throw statement.

Note
A throw statement can throw an object of any class that is a subclass of java.lang.Throwable; however, it is wise to stick with the standard convention of only throwing objects that are a subclass of class Exception.

Declaring Exceptions

A method's throws clause lists the types of exceptions that can be thrown during a method's execution. The throws clause appears immediately before a method's body in the method declaration. For example, the following method throws the ExampleException:

public void exampleMethod() throws ExampleException {
 throw new ExampleException();
}

When more than one exception may be thrown during the execution of a method, the exceptions are separated by commas in the throws clause. For example, the following method can throw either the Test1Exception or the Test2Exception:

public void testMethod(int i) throws Test1Exception, Test2Exception {
 if(i==1) throw new Test1Exception();
 if(i==2) throw new Test2Exception();
}

The types identified in the throws clause must be capable of being legally assigned to the exceptions that may be thrown.

Declare or Catch?

If a program exception can be thrown during the execution of a method, the method must either catch the expression or declare it in the throws clause of its method declaration. If an exception is not caught, it must be declared, even if it is thrown in other methods that are invoked during the method's execution.

For example, suppose that method A of object X invokes method B of object Y, which invokes method C of object Z. If method C throws an exception, it must be caught by method C or declared in method C's throws clause. If it is not caught by method C, it must be caught by method B or declared in method B's throws clause. Similarly, if the exception is not caught by method B, it must be caught by method A or declared in method A's throws clause. The handling of exceptions is a hierarchical process that mirrors the method invocation hierarchy (or call tree). Either an exception is caught by a method and removed from the hierarchy or it must be declared and propagated back through the method invocation hierarchy.

Note
Because runtime exceptions can occur almost anywhere in a program's execution, the catch-or-declare requirement applies only to program exceptions.

The CDrawApp programs of Chapters 5, "Classes and Objects," and 6, "Interfaces," provide an extended example of the declaration of uncaught exceptions. The jdg.ch05.KeyboardInput class contains three access methods that use the readLine() method: getChar(), getText(), and getInt(). The readLine() method is inherited from the DataInputStream class. It throws an exception of class IOException. Because the getChar(), getText(), and getInt() methods invoke the readLine() method, they must either catch the exception or declare it in their throws clauses. None of these methods catches IOException; therefore, all declare it in their throws clauses. The getCommand() method of class CDraw invokes the getChar() method of an object of class KeyboardMethod. It does not catch IOExeption, so it also must declare it. Because the run() method of class CDraw invokes the getCommand() method, it too is faced with catching or declaring IOException. Because run() declares IOException and the main() method of CDrawApp invokes the run() method for a CDraw object, it also must declare IOException in its throws clause.

At this point you are probably coming to the conclusion that it is a lot easier to catch and handle an exception than to declare it throughout the class hierarchy. If so, you have discovered a key benefit of Java's exception-handling approach. Java makes it easier to develop more-reliable software and harder to develop less-reliable software. If you are a lazy programmer like me, Java will exploit your tendency to do things the easy way to encourage you to do things the right way.

Using the try Statement

Statements for which exception processing is to be performed are surrounded by a try statement with a valid catch or finally clause. The syntax of the try statement is as follows:

try TryBlock CatchClauses FinallyClause;

At least one catch clause or finally clause must be defined. More than one catch clause may be used, but no more than one finally clause may be identified.

The try block is a sequence of Java statements that are preceded by an opening brace ({) and followed by a closing brace (}).

The catch clauses are a sequence of clauses of the form:

catch (Parameter) {
/*
* Exception handling statements
*/
}

Parameter is a variable that is declared to be a class or interface. The statements within the catch clause are used to process the exceptions that they "catch," as I'll explain shortly.

The finally clause identifies a block of code that is to be executed at the conclusion of the try statement and after any catch clauses. Its syntax is as follows:

finally {
/*
* Statements in finally clause
*/
}

The finally clause is always executed, no matter whether an exception is thrown.

Catching Exceptions

The try statement executes a statement block. If an exception is thrown during the block's execution, it terminates execution of the statement block and checks the catch clauses to determine which, if any, of the catch clauses can catch the thrown exception. If none of the catch clauses can catch the exception, the exception is propagated to the next higher level try statement. This process is repeated until the exception is caught or no more try statements remain.

A catch clause can catch an exception if its argument may be legally assigned the object thrown in the throw statement. If the argument of a catch clause is a class, the catch clause can catch any object whose class is a subclass of this class. If the argument to a catch clause is an interface, the catch clause can catch any object that implements that interface.

The try statement tries each catch clause, in order, and selects the first one that can catch the exception that was thrown. It then executes the statements in the catch clause. If a finally clause occurs in the try statement, the statements in the finally clause are executed after execution of the catch clause has been completed. Execution then continues with the statement following the try statement.

The following example shows how catch clauses are used to process exceptions that are thrown within the try statement. Create a directory ch07 under c:\java\jdg and enter the source code in the file ExceptionTest.java. Compile it using the command javac ExceptionTest.java. The source code for the ExceptionTest program is shown in Listing 7.1.


Listing 7.1. The source code of the ExceptionTest program.

import jdg.ch05.KeyboardInput;
import java.lang.System;
import java.lang.Exception;
import java.io.IOException;

class VowelException extends Exception {}
class BlankException extends Exception {}
class ExitException extends Exception {}

class ExceptionTest {
 static KeyboardInput kbd = new KeyboardInput(System.in);
 public static void main(String args[]) {
  boolean finished = false;
  do {
   try {
    processUserInput();
   }catch (VowelException x) {
    System.out.println("A vowel exception occurred.");
   }catch (BlankException x) {
    System.out.println("A blank exception occurred.");
   }catch (ExitException x) {
    System.out.println("An exit exception occurred.");
    finished = true;
   }finally {
    System.out.println("This is the finally clause.\n");
   }
  } while(!finished);
 }
 static void processUserInput() throws VowelException, BlankException,
  ExitException {
  System.out.print("Enter a character: ");
  System.out.flush();
  char ch;
  try {
   ch=Character.toUpperCase(kbd.getChar());
  } catch (IOException x) {
   System.out.println("An IOException occurred.");
   return;
  }
  switch(ch) {
   case 'A':
   case 'E':
   case 'I':
   case 'O':
   case 'U':
    throw new VowelException();
   case ' ':
    throw new BlankException();
   case 'X':
    throw new ExitException();
  }
 }
}


The ExceptionTest program uses the jdg.ch05.KeyboardInput class to retrieve a character entered by the user. It then throws and catches a VowelException, BlankException, or ExitException based on the user's input.

The ExceptionTest class consists of a single class variable, kbd, that is statically initialized to an object of class KeyboardInput, with System.in as an argument to its constructor.

ExceptionTest provides two static methods, main() and processUserInput(). The main() method consists of a simple do statement that repeatedly tries to invoke processUserInput(). The try statement has three catch clauses and a finally clause. The three catch clauses notify the user of the type of exception they catch. The catch clause with an ExitException parameter causes the do statement and the program to terminate by setting finished to true. The finally clause just displays the fact that it has been executed.

The processUserInput() method prompts the user to enter a character. The actual reading of the character occurs within a try statement. IOException is caught by the try statement, eliminating the need to declare the exception in the processUserInput() throws clause. The IOException is handled by notifying the user that the exception occurred and continuing with program execution.

The processUserInput() method throws one of three exceptions based upon the character entered by the user. If the user enters a vowel, VowelException is thrown. If the user enters a line beginning with a non-printable character, BlankException is thrown. If the user enters x or X, ExitException is thrown.

To run ExceptionTest, type javac ExceptionTest:

C:\java\jdg\ch07>java ExceptionTest
Enter a character:

The program prompts you to enter a character. Enter a blank line, and the following output is displayed:

A blank exception occurred.
This is the finally clause.

Enter a character:

The program notifies you that a blank exception has occurred and displays the fact that the finally clause of the main() try statement was executed. The processUserInput() method, upon encountering a space character returned by getChar(), throws the BlankException, which is caught by the main() method. The finally clause always executes no matter whether processUserInput() throws an exception or not.

Enter a at the program prompt, and the following output appears:

Enter a character: a
A vowel exception occurred.
This is the finally clause.

Enter a character:

Here the program notifies you that a vowel exception has occurred. The processing of the vowel exception is similar to the blank exception. See if you can trace the program flow of control involved in this processing.

Enter j, and the following is displayed:

Enter a character: j
This is the finally clause.
Enter a character:

No exceptions are thrown for the j character, but the finally clause is executed. The finally clause is always executed, no matter what happens during the execution of a try statement. Go ahead and type x to exit the ExceptionTest program. The program displays the following output:

Enter a character: x
An exit exception occurred.
This is the finally clause.

The exception then returns you to the DOS prompt.

The output acknowledges the fact that the exit exception was thrown by processUserInput() and caught by main().

The ExceptionTest program provides a simple example of exception throwing and catching. The example in the following section illustrates more complex exception handling.

Nested Exception Handling

try statements can be nested to provide multiple levels of exception-handling capabilities. This is accomplished by enclosing a method or block of statements containing a lower-level try statement within the try block of a higher-level try statement. When an exception is thrown in the try block of the lower-level try statement that cannot be caught, it continues to be thrown until it reaches the higher-level try statement. The higher-level try statement can then determine whether the exception can be caught and processed by any of its catch clauses. Any number of try statements can be nested. Figure 7.1 illustrates this concept.

Figure 7.1 : Nested exception handling: An exception generated within the lower-level try statement is first passed to its catch clause(s). If it is not handled, it is propagated to the higher-level catch clause(s). If it is not handled by the higher-level catch clause(s), it is propagated further up the exception-handling hierarchy.

Rethrowing Exceptions

When an exception is caught in the catch clause of a try statement, that exception may be rethrown. When an exception is rethrown, it can then be caught and processed by the catch clause of a higher-level try statement. A higher-level catch clause can then perform any secondary clean-up processing.

The following example illustrates nested exception handling and the rethrowing of exceptions. Enter the source code shown in Listing 7.2 into NestedExceptionTest.java and compile it.


Listing 7.2. The source code of the NestedExceptionTest program.

import jdg.ch05.KeyboardInput;
import java.lang.System;
import java.lang.Exception;
import java.io.IOException;

class VowelException extends Exception {}
class BlankException extends Exception {}
class ExitException extends Exception {}

class NestedExceptionTest {
 static KeyboardInput kbd = new KeyboardInput(System.in);
 public static void main(String args[]) {
  do{} while(!exitExceptionTest());
 }
 static boolean exitExceptionTest() {
  try {
   vowelExceptionTest();
   System.out.println("Acceptable.\n");
  }catch (ExitException x) {
   try {
    System.out.print("Exit (y/n): ");
    System.out.flush();
    char ch = Character.toUpperCase(kbd.getChar());
    System.out.println();
    if(ch=='Y') return true;
    else return false;
   }catch (IOException iox) {
    return false;
   }
  }catch (Exception x) {
   System.out.println("Not acceptable. Try again.\n");
  }
  return false;
 }
 static void vowelExceptionTest() throws VowelException, ExitException {
  try {
   blankExceptionTest();
  }catch (BlankException x) {
   System.out.println("Next time type a printable character.\n");
   vowelExceptionTest();
  }catch (VowelException x) {
   System.out.println("You typed a vowel.");
   throw x;
  }
 }
 static void blankExceptionTest() throws VowelException, BlankException,
  ExitException {
  try {
   processUserInput();
  }catch (BlankException x) {
   System.out.println("You entered a blank line. Try again.");
   throw x;
  }
 }
 static void processUserInput() throws VowelException, BlankException,
  ExitException {
  System.out.print("Enter a character: ");
  System.out.flush();
  char ch;
  try {
   ch=Character.toUpperCase(kbd.getChar());
  } catch (IOException x) {
   System.out.println("An IOException occurred.");
   return;
  }
  switch(ch) {
   case 'A':
   case 'E':
   case 'I':
   case 'O':
   case 'U':
    throw new VowelException();
   case ' ':
    throw new BlankException();
   case 'X':
    throw new ExitException();
  }
 }
}


This example is based on the previous example, but it is significantly more complex. The exception handling has been removed from the main() method and distributed among three nested exception handlers: exitExceptionTest(), vowelExceptionTest(), and blankExceptionTest().

The main() method invokes exitExceptionTest() at each iteration of the do statement. The exitExceptionTest() method returns a boolean value indicating whether the do statement should terminate. The normal (non-exception) processing performed by exitExceptionTest() consists of the following three statements:

vowelExceptionTest();
System.out.println("Acceptable.\n");
return false;

All other exitExceptionTest() processing is error handling. The first two statements are executed within the try statement and are subject to error processing. The last statement executes upon completion of the try statement, assuming that no transfer of control occurs as the result of exception handling involving the catch clauses.

The try statement has two catch clauses. The first catch clause handles ExitException processing. It consists of a try statement with a catch clause that catches the pesky IOException. The try statement contains a block of statements that asks the user for a confirmation before exiting the program.

The second catch clause catches all other objects that are instances of a subclass of Exception. It displays a short warning to the user.

The vowelExceptionTest() method consists of a try statement that invokes blankExceptionTest(). The rest of the processing performed by vowelExceptionTest() is exception handling. It catches two exceptions: BlankException and VowelException. It handles BlankException by warning the user to type a printable character and reinvoking itself. It handles the vowel exception by notifying the user that he typed a vowel and rethrowing the VowelException. By rethrowing the exception, it allows exitExceptionTest() to perform additional exception handling. Because vowelExceptionTest() rethrows the VowelException, it must declare it in its throws clause. It also must declare the ExitException because the ExitException is declared in the throws clause of blankExceptionTest() and is not caught by vowelExceptionTest().

The blankExceptionTest() simply invokes processUserInput() as part of its normal processing. It handles one exception thrown by processUserInput(): the BlankException. It handles the BlankException by informing the user that he typed a blank line and that he should try again. It then rethrows the BlankException so that it can be rehandled by vowelExceptionTest().

The processUserInput() method performs in the same manner as described in the previous example.

Analysis of NestedExceptionTest

If NestedExceptionTest seems overly complex, don't worry-it was meant to be. Its purpose is to give you a good understanding of the complex ways that exception handlers can be nested and how exceptions are rethrown. Go ahead and run NestedExceptionTest using the command java NestedExceptionTest:

C:\java\jdg\ch07>java NestedExceptionTest
Enter a character:

When you run NestedExceptionTest, the main() method invokes exitExceptionTest(), which invokes vowelExceptionTest(), which invokes blankExceptionTest(), which invokes processUserInput(). The processUserInput() method prompts you to enter a character and then does one of four things, depending on the character you enter. If you enter a vowel, it throws a VowelException. If you enter a nonprintable character or blank line, it throws a BlankException. If you enter x or X, it throws an ExitException. Finally, as a default, if you enter any other printable character, it will simply return control to the blankExceptionTest() method.

Let's work through all four scenarios. First, enter j to cause normal program processing to occur:

Enter a character: j
Acceptable.

Enter a character:

The processUserInput() method returns control to blankExceptionTest(), which returns control to vowelExceptionTest(), which returns control to exitExceptionTest(). The exitExceptionTest() method informs the user that he has entered an acceptable character and returns control to the main() method, which starts another character-input cycle.

Now let's go through the case when the user enters a blank line. Just enter a blank line at the prompt:

Enter a character:
You entered a blank line. Try again.
Next time type a printable character.

Enter a character:

A blank line causes processUserInput() to throw the BlankException. This exception is caught by blankExceptionTest(). The blankExceptionTest() method handles the exception by informing the user that he has entered a blank line and that he should try again. It then rethrows the exception, and the rethrown exception is caught by vowelExceptionTest(). The vowelExceptionTest() method handles the rethrown BlankException by telling the user that he should enter a printable character the next time he is prompted. It then invokes itself, starting the character-input cycle all over.

Let's go through the case when the user enters a vowel. Enter a at the prompt:

Enter a character: a
You typed a vowel.
Not acceptable. Try again.

Enter a character:

When a vowel is entered, processUserInput() throws the VowelException. The VowelException is not caught by blankExceptionTest() and continues to be thrown until it is caught by vowelExceptionTest(). The vowelExceptionTest() method handles the exception by informing the user that he has typed a vowel and then rethrows the exception. The rethrown exception is caught by exitExceptionTest(), and exitExceptionTest() handles it by informing the user that his input is not acceptable. Execution control returns to the main() method, which starts another character-input cycle.

This last case examines what happens when the user types x at the character prompt. Enter x to see what happens:

Enter a character: x
Exit (y/n):

The processUserInput() method throws an ExitException, which is not caught by blankExceptionTest() nor vowelExceptionTest(). The exception continues to be thrown until it is caught by exitExceptionTest(). The exitExceptionTest() method prompts the user to enter a y or Y to confirm the fact that he wants to exit the program. If the user enters any other character, control returns to the main() method and another character-input cycle is initiated. If the user enters a y or Y, control is returned to the main() method, but the true return value is passed, causing the do statement and the program to terminate.

Go ahead and type y to terminate the NestedExceptionTest program.

Summary

In this chapter you have learned how to use Java exceptions to implement error-handling capabilities in your Java programs. You have learned how to throw exceptions in response to error conditions and how to catch exceptions to perform error processing. You have also learned how to implement nested exception handling and how to rethrow exceptions. In Chapter 8, "Multithreading," you will learn how to use Java's multithreading capabilities to write programs that use multiple threads of execution.