| Paul Haahr / Essays / Java Style / Errors and Exceptions |
|
|
Errors and ExceptionsDon't use exceptions for normal control flowJava is, intentionally, a safe language. The run-time system includes many safety- (and sanity-) preserving features, such as null checks, type checks, and array bounds checks, and signals errors with exceptions. Using those checks to catch your mistakes is a good thing. Relying on them for detecting normal situations isn't. The most egregious abuse of Java is use a try/catch clause combined with run-time checks, rather than simple conditionals, as in this sort of code:
int sumArray(int[] array) {
int sum = 0;
try {
for (int i = 0;; i++) // BAD: no loop test
sum += array[i];
} catch (IndexOutOfBoundsException e) {
return sum;
}
}
This example is similar to one from BYTE Magazine, which offered it as an optimization over the traditional loop over an array, because it could save a comparison per iteration. On style grounds, this is clearly awful, and turns into the worst form of spaghetti control flow very quickly. | |
|
On performance, the author's claim might have been right in the case of naïve interpreters, though the overhead for throwing and catching an exception probably dominates the cost of the extra check for all but the largest arrays. But, with even the simplest of JIT compilers, a loop such as:
for (int i = 0; i < array.length; i++)
sum += array[i];
is going to contain only one array bounds check, because the reference
to array[i] inside the loop is clearly safe.
In general, with a compiler, extra checks are ususally free and can even serve to help the compiler generate better code. For example, in the code fragment above, the code to create an exception does not have to be generated, so the result will be a smaller program. Don't use declared exceptions for erroneous conditionsThere are two categories of exceptions (more properly, subclasses of Throwable) in Java, those which need to be declared by any method which might them (I refer to these as ``declared exception types'' or ``declared execptions'') and those which can be thrown from any method (``undeclared exceptions''). The undeclared exception types are subclasses of Error and RuntimeException; all other exception types require declarations. There is a balance to be struck between the uses of declared and undeclared exceptions. Using only declared exceptions makes it very difficult to determine where exceptions that actually must be checked for are potentially thrown. On the other hand, using only undeclared exceptions makes it impossible to figure out where exceptions that should be caught and recovered from are thrown. My rule of thumb for whether I make an exception declared or undeclared is whether localized recovery from the exception being thrown is sensible. If the calling method (or one of its recent callers) of the code is the right place to handle a given failure type, I represent that failure with a declared exception. If, on the other hand, if the failure case is best handled by a global handler which catches all the exceptions for a given component of a program and treats them all as failures of that subsystem, I use undeclared exceptions. When in doubt, I err on the side of using undeclared exceptions. Declared exceptions -- because they are reflected in the signature of a method and require matching definitions -- can be used for enforcing static invariants of a program. For example, consider a method which may only be invoked at given times in a program, such as when the moon is full. That method may be declared with a throws MoonMustBeFull clause, referring to a declared exception reserved for that purpose. Then, the author of any code which calls this method is reminded of the static requirements. The method, of course, may check whether the moon is full when it is invoked, and throw the exception when it is not. But throwing the exception is almost incidental; more important is the fact that the caller must pay attention to the requirement. Let the language find errors for youJava is a safe, statically-typed, strongly-typed language. This means that it will point out many of your errors, both at compilation-time and at run-time. One of the most intriguing decisions made by the designers of Java was to eliminate warnings from the compiler. Either a construct is correct, and you can use it, or it's incorrect, and you get a warning. For example, the rules for definite assignment are specified to prevent bugs due to uninitialized local variables. This has the overall effect of making it more likely that programs will be correct when the compiler has finally agreed to compile them. The run-time checks are used to guarantee that a program doesn't violate the constraints of the language by preventing references to, for example, a field of a null object or an out-of-bounds element of an array. These checks are essential to the safety and security Java promises. However, getting such a run-time error is usually an indication of some upstream problem; very rarely is the right approach in such a case to merely insert a check to prevent the run-time error from occurring or to insert a try/catch statement which ignores such an error condition. Often such a problem is trivial to find given the run-time exception and stack traceback. Use assertions | |
|
One unfortunate absence in Java is a built-in assertion statement. Just as the language's run-time checks are used to enforce the invariants of the language, user-written code should include assertions to ensure that what the programmer believed to be true about the state of the program is actually true at run-time. Any individual piece of a large program necessarily makes assumptions about the state of the rest of the program and the context in which it is running. Assertions serve two fundamental purposes: to document those assumptions and to ensure that the conditions hold whenever the code relying on them is run. Pervasive use of assertions makes programs easier to get correct; it's always better to have an error appear as an assertion failure than letting a program run for a time in an erroneous state before failing or, worse, producing incorrect results. Like comments, good assertions are meaningful and up-to-date. Asserting that 1 != 0 is rarely helpful. Similarly, an erroneous assertion in rarely executed code may confuse more than help. When examining an assertion failure, a programmer has an obligation to determine whether the assertion or the rest of the program is correct. I typically add a method along the lines of:
public static void assert(boolean condition) {
if (!condition)
throw new Error("assertion failure");
}
to any class where I want to write an assertion. (Often such a
definition in a widely used superclass is very useful.) I always use
undeclared exceptions for assertion failures.
|