We'll cover the purpose and use of exceptions following this order. The one sentence summary is “Exceptions are like software interrupts—they are generated by error conditions like division by zero, and they divert the flow of control to a place where you have said you will handle this kind of error.”
First, we'll look at the basics of:
Why exceptions are in the language.
What causes an exception (implicitly and explicitly).
Once an exception has occurred, you'll want to know how to take steps to deal with it:
How to handle (“catch”) an exception within the method where it was thrown.
Handling groups of related exceptions.
You'll also need to know what happens if you do not provide code to handle each type of exception. You can skip this on a first reading if you just want the exception basics. These sections have information saying how methods tell the compiler about exceptions they might generate but do not handle:
Exceptions are for changing the flow of control when some important or unexpected event, usually an error, has occurred. They divert processing to a part of the program that can try to cope with the error, or at least die gracefully.
There are many possible errors that can happen in non-trivial programs: these range from “unable to open a file” to “array subscript out of range” to “no memory left to allocate” to “division by zero.” It would obscure the code greatly to check for all possibilities at all places where they may happen. Exceptions provide a clean way for you to write general or specific error handlers, and have the run-time system watch for these errors and transfer control when necessary. Your error handler code thus does not clutter up the mainstream processing.
Java exceptions are adapted from C++, which itself borrowed them from the functional language ML, developed by Bell Labs, Princeton, and recently Yale. Java exception terminology is shown in Table 10-1.
Table 10-1. Exception terminology of Java
Note |
Java |
Some other languages |
---|---|---|
An error condition that happens at run-time |
Exception |
Exception |
Causing an exception to occur |
Throwing |
Raising |
Capturing an exception that has just occurred and executing statements to resolve it in some way |
Catching |
Handling |
The block that does this |
Catch clause |
Handler |
The sequence of method calls that brought control to the point where the exception happened |
Stack trace |
Call chain |
An exception is set in motion by the program doing some illegal or invalid action, such as trying to reference through a null pointer. An exception can also be caused explicitly by the “throw” statement. Two things happen:
An exception object is instantiated to record the details of what went wrong
The run-time system then diverts the normal flow of control, to search back up the call chain for a place where you have put a statement saying you can handle this kind of exception object.
That place can be in the same method, in the method that called the one where the exception occurred, or in the one that called that method, and so on. You might want to refresh your memory on how the stack keeps track of method invocations, by looking at Figure 10-1.
Because control propagates back up the call chain, we say that the exception object is thrown. It's not really “thrown” anywhere; the exception object is really held ready to one side, while the run-time system looks further and further up the call chain for a catch clause that covers this kind of exception.
If the run-time system gets to the top where your program execution started, and no handler for the exception has yet been found, then program execution will cease with an explanatory message.
As stated previously, exceptions occur when:
The program does something illegal (common case), or
The program explicitly generates an exception by executing the throw statement (less common case)
The throw statement has this general form:
throw ExceptionObject;
The ExceptionObject
is an object of a class that extends the class java.lang.Exception.
Here is a simple program that results in an “int division by zero” exception:
class example10 {
public static void main(String[] a) {
int i=1, j=0, k;
k = i/j; // line 5 causes division-by-zero exception
}
}
Compiling and running this program gives this result:
> javac example10.java
> java example10
java.lang.ArithmeticException: / by zero
at example10.main(example10.java:5)
The message “/ by zero” is the unknown compiler writer's goofy shorthand for “division by zero”. There are a certain number of predefined exceptions, like ArithmeticException, known as the run-time exceptions. Actually, since all exceptions are run-time events, a better name would be the “irrecoverable” exceptions. The Javaheads mean “run-time” in the sense of “thrown by the run-time library code, not your code.”
Run-time exceptions contrast with the user-defined exceptions which are generally held to be less severe, and in some instances can be recovered from. If a filename cannot be opened, prompt the user to enter a new name. If a data structure is found to be full, overwrite some element that is no longer needed.
You don't have to (but can) make provisions for catching run-time exceptions. You do have to make provision for user-defined exception types. The user-defined exceptions are called checked exceptions because the compiler checks your code for consistency across: where they are thrown, the methods that say they can throw them, and where they are caught (handled).
The class hierarchy for exceptions looks like Figure 10-2.
Here is an example of how to create your own exception class by extending class java.lang.Exception:
class OutofGas extends Exception {}
class Airplane {
/* more code */
if (fuel < 0.1) throw new OutofGas();
}
Any method that throws a user-defined exception must also either catch it or declare it as part of the method interface. What, you may ask, is the point of throwing an exception if you are going to catch it in the same method? The answer is that exceptions don't reduce the amount of work you have to do to handle errors. Their advantage is they let you collect it all in well localized places in your program so you don't obscure the main flow of control with zillions of checks of return values.
Here is the general form of how to catch an exception:
The try
statement says, “Try these statements and see if you get an exception.” The try
statement must be followed by one (or both) of these two clauses:
The catch
clause
The finally
clause
You can have multiple catch
clauses, and you can have catch
clauses with a finally
clause.
Each catch
says, “I will handle any exception that matches my argument.” Matching an argument means that the thrown exception could legally be assigned to the argument exception (assignment compatible—it's the same class or a subclass of the argument exception). There can be several successive catches, each looking for a different exception. Here's an example try
statement that catches just one type of exception.
try {
i = method_A(len, 0);
j = method_B( );
} catch (CharConversionException cce) {
n = assignDefaults();
}
It's generally regarded as bad style to attempt to catch all exceptions with one clause, like this:
catch (Exception e) { /* more code */
That is too general to be of use and you might catch more than you expected. You should catch specific checked exceptions and let run-time exceptions propagate up to give you a reasonable error message when they hit the top. However, you'll see it in books and sample programs, where people are trying to avoid “code explosion” of unnecessary detail.
The finally
block, if present, is a “last chance to clean up” block. It is always executed—even if something in one of the other blocks did a “return!” The finally
block is executed whether an exception occurred or not and whether it was caught or not. It is executed after the catch
block, if present, and, regardless of the path taken, through the try
block and the catch
block.
The finally
block can be useful in the complete absence of any exceptions. It is a piece of code that is executed irrespective of what happens in the try
block. There may be numerous paths through a large and complicated try
block. The finally
block can contain the housekeeping tasks that must always be done (counts updated, locks released, and so on) when finishing this piece of code.
After the whole try ... catch ... finally series of blocks is executed, if one of those blocks doesn't divert it, execution continues after the last catch
or finally
(whichever is present). The kinds of things that could make execution divert to elsewhere are the regular things: a continue
, break
, return
, or the raising of a different exception. If a finally
clause also has a transfer of control statement, then that is the one that is obeyed.
We mentioned before that “matching an argument” means that the thrown exception can be assigned legally to the argument exception. This permits a refinement. It allows a handler to catch any of several related exception objects with common parentage. Look at this example:
class Grumpy extends Exception {}
class TooHot extends Grumpy {}
class TooTired extends Grumpy {}
class TooCross extends Grumpy {}
class TooCold extends Grumpy {}
/* more code */
try {
if ( temp > 40 ) throw (new TooHot() );
if ( sleep < 8 ) throw (new TooTired() );
}
catch (Grumpy g) {
if (g instanceof TooHot)
{System.out.println("caught too hot!"); return;}
if (g instanceof TooTired)
{System.out.println("caught too tired!"); return;}
}
finally {System.out.println("in the finally clause.");}
}
The catch clauses are checked in the order in which they appear in the program. If there is a match, then the block is executed. The instanceof
operator can be used to learn the exact identity of the exception.
If none of the catch
clauses match the exception that has been thrown, then the finally
clause is executed (if there is one). At this point (no handler for this exception), what happens is the same as if the exception was raised and there wasn't a try
statement. The flow of control abruptly leaves this method, and a premature return is done to the method that called this one. If that call was in the scope of a try
statement, then we look for a matching exception again, and so on.
Figure 10-4 shows what happens when an exception is not dealt within the routine where it occurs. The run-time system looks for a “try
. . . catch
” block further up the call chain, enclosing the method call that brought us here. If the exception propagates all the way to the top of the call stack without finding a matching exception handler, then execution ceases with a message. You can think of this as Java setting up a default catch block for you around the program entry point that just prints an error message and quits.
There is no overhead to putting some statements in a try
statement. The only overhead comes when an exception occurs.
Earlier we mentioned that a method must either catch the exception that it throws or declare it along with its signature,[1] meaning it must announce the exception to the outside world. This is so that anyone who writes a call to that method is alerted to the fact that an exception might come back instead of a normal return. It also allows the compiler to check that you are using exceptions consistently and have covered all the cases.
When a programmer sees that a method can raise an exception, he or she makes the choice between handling the exception or allowing it to propagate further up the call stack. A method declares the exceptions that might be thrown out of it by listing them in a throws clause like this:
public static int parseInt(String s, int radix)
throws NumberFormatException {
/* body of method */
}
If the routine can throw more than one exception, list them all, separating the names with commas. The names must be exception or error names (that is, any type that is assignable to the predefined type Throwable). Note that just as a method declaration specifies the return type, it specifies the exception type that can be thrown, rather than an exception object.
The rules for how much and what must match, when one method that throws an exception overrides another, work in the obvious way. Namely, if you never do this, you will never obviously be bothered by it. Well, OK, another way to think about it is to consider the exception as an extra parameter that must be assignment-compatible with the exception in the class being overridden.
When you create a new exception by subclassing an existing exception class, you have the chance to associate a message string with it. The message string can be retrieved by a method. Usually, the message string will be some kind of more detailed indication about what's gone wrong.
class OutofGas extends Exception {
OutofGas(String s) {super(s);} // constructor
}
/* more code */
// in use, it may look like this
try {
if (fuel<1) throw new OutofGas("less than 1 litre fuel");
}
catch ( OutofGas o) {
System.out.println( o.getMessage() );
}
At run-time, if there is less than one unit of fuel where it is tested, the OutOfGas exception will be thrown, and the handler will cause this message to appear:
less than 1 litre fuel
You might log the message, or write it to a diagnostics file instead of displaying it. Another method that is inherited from the superclass Throwable is printStackTrace()
.
Invoking this method on an exception will cause the call chain at the point where the exception was thrown (not where it is being handled) to be printed out. For example:
// catching an exception in a calling method
class test5p {
static int myArray[] = {0,1,2,3,4};
public static void main(String[] a) {
try {
bob();
} catch (Exception e) {
System.out.println("caught exception in main()");
e.printStackTrace;
}
}
static void bob() {
try {
myArray[-1] = 4; //obvious out of bounds exception
}
catch (NullPointerException e) {
System.out.println("caught a different exception");
}
}
}
We have a handler for null pointer, but that has nothing to do with the exception that occurred. So the exception propagated back to the top and execution ended. At run-time it will produce output like this:
caught exception in main()
java.lang.ArrayIndexOutOfBoundsException: -1
at test.bob(test5p.java:19)
at test.main(test5p.java:9)
Their purpose is to allow safer programming by providing a distinct path to deal with errors.
Many methods in the API throw exceptions, so you can't avoid using them. Do define your own error conditions. But don't over-use them and make them do the work that an if statement should do. Exceptions are a useful tool for organized handling of true error conditions.
The main use of exceptions is getting a decent error message explaining what failed, where, and why. It's a bit much to always expect recovery. Graceful degradation is often the most you can obtain.
We will next review the assert statement.
[1] The exceptions a method throws are not part of the signature, though.
52.15.56.12