In my last article, I showed how the substitution of null pointers for various base
types of data is one of the most common causes of NullPointerExceptions.
This time, I will show how substituting null pointers for exceptional
conditions also tends to cause problems. Exceptional
conditions in Java programs are ordinarily dealt with by throwing exceptions and
catching them at an appropriate point of control. But you will often see
methods that indicate such a condition by returning a
null-pointer value (and, perhaps, printing a message to System.err). If the
calling method does not explicitly check for a null pointer, it may attempt
to dereference the return value and trigger a null-pointer
exception.
As you might have guessed, I call this pattern the Null Flag bug pattern because it is caused by inconsistently using null pointers as flags for exceptional conditions.
Let's consider the following simple bridge class from BufferedReaders to Iterators:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.util.Iterator;
public class BufferedReaderIterator implements Iterator {
private BufferedReader internal;
public BufferedReaderIterator(BufferedReader _internal) {
this.internal = _internal;
}
public boolean hasNext() {
try {
boolean result = true;
// Let's suppose that lines in the underlying input stream are known
// to be no greater than 80 characters long.
internal.mark(80);
if (this.next() == null) {
result = false;
}
internal.reset();
return result;
}
catch (IOException e) {
System.err.println(e.toString());
return false;
}
}
public Object next() {
try {
return internal.readLine();
}
catch (IOException e) {
System.err.println(e.toString());
return null;
}
}
public void remove() {
// This iterator does not support the remove operation.
throw new UnsupportedOperationException();
}
} |
Because this class serves as a bridge implementation of the
Iterator interface, the code must catch IOExceptions from BufferedReader. Each of the methods handles IOExceptions by returning some default
value. In the case of hasNext, the value false is
returned. This is reasonable because, if an IOException is thrown, the client
should not expect that another element can be retrieved from the Iterator. On
the other hand, next simply returns null, both in
the case of an IOException and, because it relies on the return value of
internal.readLine(), in the case when internal is
empty. But this is not what the client of an Iterator object
would expect. Normally, when next is called on an
Iterator with no more elements, it throws a NoSuchElementException. If a
client of our Iterator is relying on this behavior, it may very likely attempt
to dereference a null pointer returned from a call to next, resulting
in a NullPointerException.
Whenever a NullPointerException occurs, check for scenarios like the one
described above. Occurrences of this bug pattern are very common.
Despite their common occurrence, the use of null flags is very often
unwarranted (as in the case above). Let's rewrite next so that it
throws NoSuchElementException as expected:
public Object next() {
try {
String result = internal.readLine();
if (result == null) {
throw new NoSuchElementException();
}
else {
return result;
}
}
catch (IOException e) {
// The original exception is included in the message to notify the
// client that an IOException has occurred.
throw new NoSuchElementException(e.toString());
}
}
|
Note that, to make the rest of the code work with this altered method, we would also have to:
- Import
java.util.NoSuchElementException. - Fix
hasNextso that it no longer callsnextto test. The simplest way to do this would be to simply callinternal.readLine()directly.
An alternative to our handling of IOExceptions would be to catch them and
throw RuntimeExceptions in their stead. Base your decision to do this on an
assessment of how frequently IOExceptions are expected to occur on the target
platform. If they will be frequent, then you might want to try recovering from
them.
Any code that calls this new next method may have to deal with
thrown NoSuchElementExceptions. Of course, the code may simply choose to ignore them
and allow the program to abort. If so, the resultant error message and the
place where the exception was thrown would be much more informative than a
NullPointerException thrown by the original code. If the exception thrown were
a checked exception (such as IOException) it would be even more
helpful, because no client code for the class would compile unless it handled the
exception. That way, we could eliminate even the possibility of certain errors
occurring before the program is ever run. But, in this example, we couldn't
throw such a checked exception without violating the Iterator
interface. So, we've sacrificed some static checking for the sake of reusing
code that operates on Iterators. Tensions such as these, between
the goals of static checking and reuse, are quite common.
Before I leave this topic, I should address the concerns of many programmers who use null flags regularly. Many programmers argue that it makes their programs more "robust." After all, they might say, a robust system handles varied situations gracefully rather than throwing exceptions at every little problem that comes up. But this argument overlooks the fact that exceptions are a great tool to increase the robustness of code, by allowing control to pass quickly during an exceptional condition to the place most appropriate for controlling it. The use of null flags, on the other hand, limits control flow to the ordinary paths of method invocation and return (until the whole program crashes, of course). Additionally, by using null flags in this way, the programmer effectively buries the evidence of where the exceptional condition occurred. Who knows how far that null pointer was passed from method to method before it was ever dereferenced? This simply makes it harder to diagnose errors and determine how to fix them. Experience has shown that code breaks often. Our primary concern should be to avoid such obfuscation and make diagnosis as easy as possible. Therefore, as a matter of principle, I strive to write code that signals exceptional conditions as soon as possible and attempts to recover only from exceptional conditions that do not indicate bugs in the program.
Even if you try to avoid using null flags in your own code, you will
inevitably have to deal with legacy code that uses them. In fact, many
of the Java library classes themselves, such as the Hashtable class and the
BufferedReader class that we used above, use null flags. When
using such classes, you can avoid bugs by explicitly checking whether an
operation will return null before performing it. For example, with Hashtables,
I always test with containsKey before calling get.
But, even with such preventative measures, this bug pattern is one of the most
common patterns encountered.
Here's the breakdown of this week's bug pattern:
- Pattern: Null Flag
- Symptoms: A code block that uses null pointers as flags for exceptional
conditions is signalling a
NullPointerException. - Cause: The calling methods are not checking for null pointers as return values.
- Cures and preventions: Throw exceptions to signal exceptional conditions.
In the next article, I will discuss bug patterns related to class cast exceptions.
- Be sure to read Eric Allen's previous Diagnosing Java Code columns on bug patterns:
- The Dangling Composite bug pattern (developerWorks, March 2001)
- Bug patterns: An introduction (developerWorks, Februray 2001)
- One way to prevent inconsistent handling of exceptional
conditions is aspect-oriented programming: a style of programming that
modularizes the parts of a program that ordinarily cut across module
boundaries. Check out
AspectJ,
an aspect-oriented extension of the Java language, with an implementation that supports
many popular Java IDEs.
- An approach to statically determining possible null-pointer exceptions is a technique known as set-based analysis. The Carnegie Mellon School of Computer Science Web site provides a short introduction to this approach, along with links to several technical publications on the topic.
- The Division of Software Engineering at DePaul University has done some work on automated theorem provers to detect null-pointer exceptions in Java code.
- Visit the Patterns Home
Page for a good introduction to design patterns and how they
are used.
- Check out JUnit and catch more
errors by making your code "test-infested."
Eric Allen has an A.B. in computer science and mathematics from Cornell University. He is currently the lead Java software developer at Cycorp, Inc., and a part-time graduate student in the programming languages team at Rice University. His research concerns the development of formal semantic models and extensions of the Java language, both at the source and bytecode levels. Currently, he is implementing a source-to-bytecode compiler for the NextGen programming language, an extension of the Java language with generic run-time types.