Exceptions are a language mechanism that helps transfer control from one point in the program to another without cluttering the code in between. As always, we should keep a clear distinction between the mechanisms the programming language designers chose to put at our disposal and the proper ways to use those mechanisms to write good code.
In particular, an exception should not be thought of as exactly the same thing as an error, although exceptions are often used to indicate errors. We will use the word “error” to mean a mistake in the code: a programmer error. One reason why the ideas of exceptions and errors are confused is that exceptions are a useful way to stop programs quickly and cleanly when a programmer error is detected.
Exceptions have another use, however. We may in some cases want to use exceptions to handle unusual conditions within the code. These might be “errors” in (that is, misconfigurations of) the environment in which the program is being run rather than mistakes by the programmer. We want the program to be able to handle such unusual conditions and respond accordingly; exceptions are a nice way to handle such unusual conditions fairly cleanly. Without exceptions, the code to handle unusual conditions ends up mixed in with the code that handles normal-case execution of the program. This mixing makes the normal-case code (and hence the code as a whole) harder to understand. With exceptions we can factor out separately the code that handles unusual conditions.
Exceptions are generated either by using the throw statement to
throw an object of a subclass of class Throwable, or by using a
built-in operation that generates an exception under some condition. Java has
a quite a few builtin exceptions that can be generated by standard language
constructs. Null values generate a NullPointerException if used as
objects, arrays generate an ArrayIndexOutOfBoundsException if the
array index is, well, out of bounds, and so on.
Exceptions can also be caught by using a try-catch
statement. It has a body defining what code is allowed to
generate an exception, then at least one catch clause (and possibly
a finally clause). The catch clauses define which
exceptions generated by the try body will be caught and define
what code is run when the exception is caught. The finally block
provides some code that is always run before the try statement
finishes, whether or not an exception was generated.
The finally block is very useful for performing cleanup
work that must happen regardless of how cleanly the try statement
completes. For example, it might close files that were opened in the main body
of the try.
Here is a small example of using exceptions to separate handling of unusual conditions
from normal-case code. This code parses the command line of the program by scanning
the argument list from left to right. However, it needs to handle the case in which
the user fails to provide a filename to the --file command-line option.
The code can be simpler if it doesn't have to check
that every index into the array of arguments is in bounds.
As the code below demonstrates, it is possible to use try...catch to
factor out the handling of that problem in an exception handler:
arglist.java
In a typical command-line parser, there will be multiple option for which a user might forget to supply the corresponding argument. Code in the style above not only avoids cluttering up the normal-case code with error handling, but it even consolidates the handling of multiple errors into one place.
An alternative to using exceptions is to define
special return values to indicate unusual conditions. The Java libraries often
(unfortunately) follow this strategy. For example, the specification for
String.indexOf looks like this:
/** * Returns: if the string argument occurs as a substring within this * object, then the index of the first character of the first * such substring is returned; if it does not occur as a * substring, -1 is returned. */ public int indexOf(String str)
The problem with the special-return-value strategy is that it's easy to forget to check for the special value, writing code such as this:
String s1 = s2.substring(s2.indexOf("header:") + 7);
If the string doesn't begin with header:, we'll get a result that
includes the 6th and following characters of s2. This
doesn't make much sense! If the library designers had instead chosen to
throw an exception, client code could look like that above, and the compiler
would force clients to remember to check for the case in which “header:”
isn't found.
Java requires that some exceptions be declared in methods that might
generate them. These are called checked exceptions. Checked
exceptions force the client to be aware that they might happen
and to handle them appropriately. This helps lead to more robust
code. Unchecked exceptions include the run-time exceptions
like NullPointerException but also subclasses of
Error. The compiler will not warn clients if they are
ignoring unchecked exceptions.
When should you use each kind of exception? It depends on why the exception is being used:
If the exception is thrown because there is a programmer error, unchecked exceptions
are the right choice. Particularly, Error or a subclass (e.g.,
AssertionError) should be used.
If the exception is thrown because there is an unusual condition, a checked exception
should be used. The client should be aware that the condition can happen.
Sometimes checked exceptions can be annoying because the programmer “knows”
they cannot happen, yet they must be declared. The simplest way to deal with this is
to write a catch clause for those exceptions, and throw an Error from
the handler.
The coefficient method is an interesting case, because it is a
partial function that has no natural result in the case where the
requested exponent is negative. There are several alternative ways to deal with
this situation, with varying tradeoffs in terms of performance vs. debugging.
The alternative least friendly to the client is simply to require that the
requested exponent be nonnegative, by giving a requires clause:
/** Returns: the coefficient of the polynomial term with exponent n, or zero * if there is no such term. * Requires: n ≥ 0 */ double coefficient(int n);
What happens if the client calls coefficient(-1)? This spec doesn't say. Maybe it throws an exception, maybe it goes into an infinite loop, maybe it just returns a wrong answer. If the client makes this call, the code can do anything it likes. But that is okay. The spec is clear that the client must ensure that n is not negative. If the requires clause is violated, it is the client's fault.
A more forgiving version of the same spec uses a checks clause, which is a kind of requires clause. The difference is that the checks clause promises to check that the precondition holds, and to stop the program as cleanly as possible if it is violated. For example, the method might throw an exception that is a subclass of Error, which the client should not try to catch. However, client code that violates the precondition is still wrong code. It is still the client's fault if the precondition is violated.
/** Returns: the coefficient of the polynomial term with exponent n, or zero * if there is no such term. * Checks: n ≥ 0 */ double coefficient(int n);
A good way to implement checks clauses is by using the assert
statement. For example, the implementation of coefficient might
check this precondition as follows:
double coefficient(int n) {
assert n >= 0;
...
}
The assert statement may check any boolean condition. If Java
is used with assertions enabled (by using the -ea option, which you
should enable in your development environment), then the boolean condition is tested at run
time. If it evaluates to true, nothing happens. If it executes to
false, an error AssertionError is generated
and will halt the program at the point where the assertion failed.
It is also even more helpful when writing a checks clause to tell the programmer what exception will be thrown when the check fails, e.g.
// Checks: n ≥ 0 (assert)
or:
// Checks: n ≥ 0 (throws NegException)
This may help when debugging. However, if an exception is thrown to indicate an error, the client (caller) should not catch that exception. The exception indicates a problem in the client code that should not just be papered over.
A third way to deal with partial functions is to make them total, so that the customer is never wrong:
/** Returns: the coefficient of the polynomial term with exponent n, or zero * if there is no such term. Throws NegException if n is negative. */ double coefficient(int n) throws NegException;
Notice that now the information about the thrown exception is part
of the returns clause, and appears in the signature, indicating that
this exception is expected behavior in some situations and that
the client had better be ready to handle it when it happens. The client
can call with any arguments it likes, but the price to pay is writing
code to handel the exception. The
implementation of coefficient might be exactly
the same as when we used a “checks” clause. But with the
checks clause, the contract says that the client is in error; with
exceptions appearing in the returns clause, the client is always
“right”.
Javadoc doesn't completely support the clauses we have been describing thus far, though it has been evolving in that general direction. If you want to use Javadoc to generate HTML documentation, you will need to adapt this documentation strategy accordingly. The key is not that you need to have explicitly labeled clauses that Javadoc understands, but that you should know for each thing you write in the comment which clause it belongs to, and include all the information that should be found in the clauses that the spec of your code needs.