Remember the adage, "The whole is greater than the sum of its parts"? Single events, when combined as an interacting unit, can demonstrate many more results than the sum of results from each event on its own.
Programs can be the same way. With each new method added to a program, the set of possible flows of control through the program grows dramatically. With large programs, it can quickly grow out of control. And like a perverse magic trick, sometimes the direction you expect is not where you end up -- similar to what can happen when you overload or override methods.
The Broken Dispatch bug pattern
One of the most powerful features of an object-oriented language is inheritance polymorphism. This feature allows us to overload and override methods depending on the argument types. Like any other powerful tool, though, this feature introduces new dangers.
Although Java programmers quickly learn the rules governing which method will be called on an invocation, it can be easy in a large program to overload a method in one class with the result of breaking code that previously worked in another class. Such bugs match what I call the Broken Dispatch pattern.
This pattern can be described as follows:
- The arguments to an overloaded method, such as
foo, are passed to another method, such asgoo, that takes more general types. -
goothen invokesfoowith these arguments. - But because the static types of these arguments inside the scope of
gooare more general, the wrong version of methodfoomight be invoked.
Errors like these can be difficult to diagnose because they can be introduced simply by adding new methods (as opposed to modifying exists ones). Also, the program execution may continue for quite some time before any problems are discovered.
To illustrate the nature of this pattern, let's consider the following example code for an implementation of immutable lists from my earlier article on the "Null Flag bug pattern."
interface List {
public Object getFirst();
public List getRest();
}
class Empty implements List {
public Object getFirst() { throw new NoSuchElementException(); }
public List getRest() { throw new NoSuchElementException(); }
public boolean equals(Object that) {
return 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; }
...
}
|
In that article, we implemented linked lists as containers for these immutable lists.
Let's suppose that we're implementing linked lists in a separate package in which we know that all instances of class LinkedList will be lists of Strings. We could write the constructors to enforce this invariant as follows:
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(String _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(String _first, LinkedList _rest) {
this.value = new Cons(_first, _rest.value);
}
public Object getFirst() { return this.value.getFirst(); }
public LinkedList getRest() {
return new LinkedList(this.value.getRest());
}
public void push(String s) { this.value = new Cons(s, this.value); }
public String pop() {
String result = (String)this.value.getFirst();
this.value = this.value.getRest();
return result;
}
public boolean isEmpty() { return this.value instanceof Empty; }
public String toString() {...}
...
}
|
Suppose we write this code and all the test cases work. (Or, more, realistically, let's suppose that it doesn't work at first, but, after a few debugging cycles, we manage to get it working.)
Perhaps several months later you develop a new constructor on class Cons that takes a String representation of the list as its only argument. Such a constructor is quite useful -- it allows us to construct new lists with expressions such as the following:
new Cons("(this is a list)")
new Cons("(so is this)")
|
So, we write this constructor and all the test cases for it work. Great! But then, we notice that, mysteriously, some of the tests for methods on class LinkedList suddenly break. What's going on?
The problem is with the constructor on class LinkedList that takes a single String as its argument.
This constructor previously called the underlying constructor on class Cons. But, now that we've overloaded that constructor with a more specific method -- one that takes a String as argument -- this more specific method is invoked.
Unless the String passed to the LinkedList constructor is a valid representation of a Cons, the program will crash when trying to parse it. Worse yet, if that String just happens to be a valid representation of a Cons, the program will continue to execute with corrupt data. In that case, we would have introduced a saboteur into the data. For a discussion of saboteur data, see my last installment, "The Saboteur Data bug pattern."
The Broken Dispatch bug, like all bug patterns, is easiest to diagnose in "test-infested" code (to borrow from the terminology of Extreme Programming) in which all but the most trivial methods have corresponding unit tests. In such an environment, the most common symptom will be a test case for code you haven't touched that suddenly breaks.
When this happens, it's possibly a case of the Broken Dispatch pattern. If the test case breaks immediately after you overload another method, it almost certainly is.
If the code isn't test infested, things become more difficult. The bug might produce symptoms such as method invocations that return much more quickly than expected (and with incorrect results). Alternatively, you might find that certain events that were supposed to occur never did (because the appropriate method was never executed).
Remember though, symptoms like these can also be attributed to other bug patterns. If you encounter such symptoms, your best bet is to begin writing more unit tests, starting with the methods where the error was discovered and working back through the program execution.
The nice thing about this bug pattern is that there are some simple solutions. The most straightforward one is to upcast the arguments in the method invocation. In our example, this would mean rewriting the relevant LinkedList constructor as follows:
public LinkedList(String _first) {
this.value = new Cons((Object)_first);
}
|
Of course, this approach solves the problem only for this one call to the Cons constructor. There may be other calls where we have to upcast, and this technique might become cumbersome for clients of the Cons class. In cases like these, you have to weigh the advantage you gain from having this convenient constructor against the potential for errors it introduces.
This dilemma is yet another example of the tension that exists between expressiveness and robustness of an interface. One way to get the best of both worlds would be to replace the String constructor on Cons with a static method that takes a String and returns a new Cons object.
Here's the summary of our latest bug pattern:
- Pattern: Broken Dispatch
- Symptoms: A test case for code you haven't touched suddenly breaks, right after you've overloaded another method.
- Cause: The overloading has caused the untouched method to invoke a method other than the intended one.
- Cures and preventions: Put in explicit upcasts. Or, rethink the set of methods you're providing on your various classes.
Remember, argument structure is a key component to assuring that when you overload or override a method, the call invokes the intended method.
Next month we'll take a breather from bug patterns to tackle some other important topics. My next installment of Diagnosing Java Code will examine how tail-recursive methods can impact performace of your Java programs. And don't worry, we'll get back to bug patterns soon.
- Be sure to bookmark The Java Language Specification for a good discussion of the rules of method invocation.
- This installment of Under the Hood (JavaWorld, June 1997) by Bill Venners describes the bytecode implementation of method invocation in the JVM.
- Download JUnit and make your code "test infested."
- For more on the Extreme Programming methodology, read "XP distilled" (developerWorks, March 2001), which provides an excellent digest of this very popular agile process.
- Read Eric's complete series on diagnosing Java code.
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 of Java, and extensions of Java, both at the source and bytecode levels. Currently, he is implementing a source-to-bytecode compiler for the NextGen programming language, an extension of Java with generic run-time types. Contact Eric at eallen@cyc.com.
Comments (Undergoing maintenance)





