Not the Last Word
Naturally, adopting principles like these will help to reduce the occurrence of bug patterns in your code. You may find that some of the bug patterns discussed here are not the ones you most frequently encounter; and, to be sure, many of the bug patterns we've discussed are
As you discover that other patterns become more frequent, document them. Talk to others about them. Write articles about them and let people know.
Many new books, such as Bruce Tate's
Bitter Java
(see
Resources
), have started documenting various common bugs at the level of design. Tate also discusses many coding-level issues, including the Split Cleaner bug pattern (although Tate labels it an "anti pattern," a
Bug patterns can occur not just in Java, but in any programming language, even in markup languages, scripting languages, and query languages. The more we document such patterns, the more we can benefit from each other's experience. And in the fast-paced world of software development, leveraging the experience of others is a must. May your days be filled with many successful bug sleuthings! |
||||||||||||||||||
Chapter 15:
The
|
|||||||||||||||||||||
| Note |
The static type system is there to protect you from this type of bug. Use it.Weed
|
About This Bug PatternThis is the first bug pattern relating to NullPointerExceptions that we'll explore. (For a quick intro to the symptom of this pattern-the exception thrown by dereferencing a null pointer-step back to Chapter 8 .)
The SymptomsCode that uses a recursively defined data type-that is, a data type that is defined so that in certain base cases of the definition null pointers, instead of classes, are inserted into the composite-signals a NullPointerException .
The Cause
Consider the following (ugly) implementation of
Listing 9-1: A Dangling Composite in a Singly Linked Implementation of Lists
import java.util.NoSuchElementException; public class LinkedList { private Object first; private LinkedList rest; /** * Constructs an empty LinkedList. */ public LinkedList() { this.first = null; this.rest = null; } /** * Constructs a LinkedList containing only the given element. */ public LinkedList(Object _first) { this.first = _first; this.rest = null; } /** * Constructs a LinkedList consisting of the given Object followed by * all the elements in the given LinkedList. */ public LinkedList(Object _first, LinkedList _rest) { this.first = _first; this.rest = _rest; } }
This code is truly
It may at first appear that representing empty lists in this way makes the code simpler. After all, we've saved the effort of having to define an extra class just for empty lists. But, as we will
Let's define some
getter
and
setter
Notice that the action taken by both of the
getters
depends on whether the list is empty. This is exactly the
By this measure, this LinkedList implementation (in Listings 9-1 and 9-2) fails miserably. In fact, as mentioned earlier, our LinkedList class already contains a subtle but devastating bug-did you spot it? Listing 9-2: Defining getter and setter Methods for LinkedList
public Object getFirst() { if (! (this.isEmpty())) { return this.first; } else { throw new NoSuchElementException(); } } public LinkedList getRest() { if (! (this.isEmpty())) { return this.rest; } else { throw new NoSuchElementException(); } } public void addFirst(Object o) { LinkedList oldThis = (LinkedList)this.clone(); this.first = o; this.rest = oldThis; } public boolean isEmpty() { return this.first == null && this.rest == null; } public Object clone() { return new LinkedList(this.first, this.rest); }
Exactly what is our representation of empty lists?
We said earlier that an empty list is a
LinkedList
containing a null pointer in both fields. The zero-argument constructor sets up just such an empty list. But notice that the
Because a Dangling Composite confuses null pointers with placeholders for base cases, mistakes like this are very easy to make. To see how this bug can manifest itself as a NullPointerException , let's write an equals() method for our lists: Listing 9-3: Writing an equals() Method for the Linked Lists
public boolean equals(Object that) { // If the objects are not of the same class, then they are not equal. // Reflection is used in case this method is called from an instance of a // subclass. if (this.getClass() != that.getClass()) { return false; } else { // that instanceof LinkedList LinkedList _that = (LinkedList)that; return (this.isEmpty() && _that.isEmpty()) ((this.getFirst().equals(_that.getFirst())) && (this.getRest().equals(_that.getRest()))); } }
First, a brief aside. The first thing that can go wrong with this
equals()
method is that we will get a
NullPointerException
if it is ever called on
null
. Now, that might be what you'd expect to happen; after all,
null.equals(x)
will signal an error for any
x
, so shouldn't
x.equals(null)
signal the same error in order to preserve symmetry? In fact, the API for equals() specifies that equals() should form an equivalence relation on all "reference values" (
But the plot thickens. As developerWorks reader Paul Lewis has pointed out in the "Diagnosing Java Code" discussion forum, Sun has declared that
x.equals(null)
should return
false
for any non-null
x
. But this is in direct conflict with that part of the spec indicating that
equals()
should form an equivalence relation! What are we to do? It's
After I discussed this issue with Corky, he decided to
If it were possible to start over, it would be preferable to stipulate that equals() implements a true equivalence relation for all reference values. Clients should be responsible for checking that they're not passing in null pointers to equals() methods, just as they are responsible for not calling equals() on null pointers. However, given the tremendous amount of legacy Java codes currently in production use, Sun's planned conservative fix to the spec is well justified. So let's try to rewrite our equals() method to match the planned specification of equals() in 1.4.2. The first thing we have to do to fix our method is put a test at the beginning: Listing 9-4: Fixing the equals() Method with a Preemptive Test
// The semantics for equals specified in the Sun JDK 1.3 API require that // we return false for x.equals(null), x non-null. if (that == null) { return false; }
If control flows past this test, then, because
But perhaps the
One potential solution might be to simply represent empty lists directly as null pointers. However, this would be
On the other hand, the bug could be fixed by rewriting the single-argument constructor as
Listing 9-5: A Fix That Rewrites the Single-Argument Constructor
public LinkedList(Object _first) { this.first = _first; this.rest = new LinkedList(); }
But, as is the case with most bug patterns, it is much better to prevent bugs of this kind rather than to fix them. The fact that the code broke so easily and that even simple getters , setters , and the equals() method were all so bloated, suggests that there is a better design.
Cures and PreventionIn fact, there is a simple way to avoid Dangling Composite bugs: give each base case of the data type its own class. Instead of implementing singly linked lists as we did earlier, implement such lists with a class LinkedList that contains a field holding either an Empty or a Cons class. These classes implement a common interface, as shown in Figure 9-1.
To implement mutator() methods, the new LinkedList class serves as a container for an internal, immutable list, as shown in Listing 9-6. This approach is necessary because truly empty lists have no fields to mutate and are therefore immutable. Listing 9-6: The LinkedList Class Acts as a Container for an Immutable List
import java.util.NoSuchElementException; public class LinkedList { private List value; /** * Constructs an empty LinkedList. */ public LinkedList() { this.value = new Empty(); } /** * Constructs a LinkedList containing only the given element. */ public LinkedList(Object _first) { this.value = new Cons(_first); } /** * Constructs a LinkedList consisting of the given Object followed by * all the elements in the given LinkedList. */ public LinkedList(Object _first, LinkedList _rest) { this.value = new Cons(_first, _rest.value); } private LinkedList(List _value) { this.value = _value; } public Object getFirst() { return this.value.getFirst(); } public LinkedList getRest() { return new LinkedList(this.value.getRest()); } public void addFirst(Object o) { this.value = new Cons(o, this.value); } public boolean isEmpty() { return this.value instanceof Empty; } public boolean equals(Object that) { // The semantics for equals specified in the Sun JDK 1.3 API require that // we return false for x.equals(null), x non-null. if (that == null) { return false; } if (this.getClass() != that.getClass()) { return false; } else { // that instanceof LinkedList return this.value.equals(((LinkedList)that).value); } } }
Implementation of the immutable lists is then straightforward, as shown in Listing 9-7. Listing 9-7: Implementing the Immutable Lists
interface List {} class Empty implements List { public boolean equals(Object that) { return (that != null) && (this.getClass() == that.getClass()); } } class Cons implements List { Object first; List rest; Cons(Object _first) { this.first = _first; this.rest = new Empty(); } Cons(Object _first, List _rest) { this.first = _first; this.rest = _rest; } public Object getFirst() { return this.first; } public List getRest() { return this.rest; } public boolean equals(Object that) { // The semantics for equals specified in the Sun JDK 1.3 API require that // we return false for x.equals(null), x non-null. if (that == null) { return false; } if (this.getClass() != that.getClass()) { return false; } else { // that instanceof Cons Cons _that = (Cons)that; return (this.getFirst().equals(_that.getFirst())) && (this.getRest().equals(_that.getRest())); } } }
The logic for each method is now considerably simpler. Also notice that while it is still possible to introduce the same bug as before in the singleargument Cons constructor, the fact that we have made an explicit Empty class makes this much less likely. Additionally, any code that traverses our lists and forgets to check for the empty case will get a NoSuchElementException , as opposed to the much less helpful NullPointerException . A simple optimization to this code would be to apply the Singleton design pattern to class Empty because every instance of Empty is identical. I have left out this optimization because it is not relevant to eliminating NullPointerExceptions and it makes the code slightly more complex. |
|||||||||||||||||||||||||||||||||||