 | Level: Introductory Eric Allen, Software Engineer, Cycorp, Inc.
01 Mar 2001 Often, our efforts to develop robust programs by substituting null pointers for exceptional conditions actually limits control flow to the ordinary paths of method invocation and return, and buries evidence of where an exceptional condition occured. In this column, Eric Allen shows how this bug pattern, which he calls the Null Flag bug pattern, produces unexpected results that are difficult to debug. As with the other bug patterns that we have discussed, you can minimize occurrences of this pattern by applying certain programming techniques.
The Null Flag bug pattern
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.
The cause
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.
Prevention
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
hasNext so that it no longer calls next to
test. The simplest way to do this would be to simply call
internal.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.
Wrapup
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.
Resources - Be sure to read Eric Allen's previous Diagnosing Java Code columns on bug patterns:
- 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."
About the author  | |  | 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. |
Rate this page
|  |