Java language interfaces are a great tool. They provide many of the advantages of multiple inheritance without all of the problems. Specifying an interface for all the services that a client expects to use makes it possible to plug in various implementations of that interface as necessary.
Unfortunately, the only parts of a specification that can be expressed are method signatures. There may very well be many other invariants that are expected to hold for any implementation, but the Java language provides no facility to check them.
The Fictitious Implementation bug pattern
Because of this limitation, it is possible to "implement" an interface without actually meeting its intended semantics. Bugs that result from such fictitious implementations are the topic of this week's column.
For example, consider the following interface for stacks:
Listing 1. An interface for stacks
public interface Stack {
public Object pop();
public void push(Object top);
public boolean isEmpty();
}
|
Any class containing methods that match the above signatures would, from the perspective of the Java type checker, serve as a legal implementation of a Stack. But in practice there are several additional requirements we would expect a stack to fulfill. For instance:
- If an object
ois pushed on a stacks, and the next operation performed on the stack ispop, then the return value of that operation should beo. - If, for a given stack
s, the return value ofs.isEmpty()istrue, and the next operation performed on the stack ispop, then that call topopshould throw aRuntimeException.
There are lots of other invariants we could specify. How do we expect a stack to handle multiple push operations? What behavior do we expect with multiple threads? It is difficult to enforce invariants such as these programmatically. We could (and should) mention them in the documentation, but a developer writing an implementation could easily ignore them. If that happens, then a client that relies on such invariants will not work with such an implementation, and we'll have a bug. I call bugs of this pattern fictitious implementations because I place the blame for them squarely on the implementation rather than the client. Like any bug that deserves its own pattern, a fictitious implementation may not be immediately apparent, but can lurk hidden until some uncommon execution path uncovers it.
The Java language is not to blame!
Before continuing, I should mention that I am not criticizing the Java language's inability to specify such invariants. Any mechanism that allowed for such specification would have many accompanying disadvantages. For one thing, many of the invariants that we would like to specify cannot be checked statically. Although type signatures express only a small set of invariants, they are easy to check compared to stipulations like the ones we outlined above for stacks.
There's another disadvantage to allowing for more expressive specifications in interfaces: in doing so, we could easily burden the Java language with many of the problems that plague languages with full multiple inheritance. Consider the following interface:
Listing 2. An interface for poppers
public interface Popper {
public Object pop();
}
|
Let's suppose that the pop() method in this interface has an intended invariant: a call to pop() should never throw a RuntimeException. Now, with the current Java interface facility, it would be possible for a class to implement both Stack and Popper. But, according to the intended specifications, the implementations of pop() in each class would be mutually incompatible.
If we wanted to continue to allow a class to implement both interfaces, we would have to provide the ability to overload a method based not just on type signature, but also on the extra kinds of invariant specifications we add to the language. But this would introduce a serious problem: how would we determine, at a given method invocation, which version of the method to invoke? Normally, this is done by determining the static types of the method arguments; but with extra invariants, such a technique would not be sufficient. This problem is very similar to one experienced with full multiple inheritance: if more than one parent class defines a method with the same name and type signature, how do we disambiguate calls to these methods? There are many ways to allow for manual disambiguation of such calls, but they all tend to add a great deal of complexity to a language. What's worse, schemes to automatically disambiguate such calls are themselves complex and prone to error, as programmers will often make mistakes when predicting which method will be called.
So, it is a perfectly justifiable design choice to restrict the specification of interface invariants to type signatures. And we don't need to loosen that restriction to detect and correct fictitious implementations.
Detecting fictitious implementations
The main problem with fictitious implementations is, of course, that they will pass compilation without incident. The symptoms at run time will often be very puzzling because the extra invariants a programmer expects an interface to satisfy are often left unspoken; the programmer may not even be consciously aware that he is expecting them to be satisfied. The process of correcting a bug often starts with a period of confusion; and the programmer tripped up by the fictitious implementation pattern may at first try to convince himself that the problem he has observed cannot possibly have occurred. If you find yourself in this situation, it's a good time to check your premises. What hidden assumptions have you not stated? How can you test these assumptions to thoroughly eliminate the possibility that they are faulty? If you rely on an interface to another part of the system, and the implementation of that interface has been modified since the last release, then you might be running up against a fictitious implementation.
In cases such as these, it is important that the maintainer of the interface document any invariants that may be assumed by a client programmer. If a client programmer discovers that an invariant he was relying on was not, in fact, documented, then the client programmer and the maintainer of the interface should sit down and discuss whether that invariant should be made explicit. Often, it will be easy to add an assumed invariant to the specification, saving the client the trouble of modifying all the code that relied on it.
If the maintainer of the interface is not available, then the client cannot rely on anything but the documented invariants of the interface. If these are scant, then the interface is much less valuable than it could be. If the client chooses to rely on undocumented invariants, the client code he writes can quickly lose value itself, since it may be incompatible with future releases of the interface implementation.
If you're designing an interface, you have two very powerful tools to prevent bugs of this pattern: unit tests and assertions. In Part 2 of this series, I will discuss how these technologies can be used as a sort of executable documentation to aid in enforcing interface invariants.
- Clemens Szyperski discusses the distinction between interface and implementation inheritance in his excellent book Component Software: Beyond Object-Oriented Programming.
- The JUnit home page provides links to many interesting articles discussing program testing methods, as well as the last version of JUnit.
- "The UML Profile for Framework Architectures" (PDF slide show) highlights a detailed case study of JUnit.
- Take the Java debugging tutorial (developerWorks, February 2001) for help with general debugging techniques.
- New to Java development or looking to brush up on your Java programming skills? Take this comprehensive tutorial: Introduction to Java programming (developerWorks, November 2004).
- Read all of Eric's Diagnosing Java Code articles, many of which focus on bug patterns.
- Find more Java resources on the developerWorks Java technology zone.
Eric Allen has an A.B. in computer science and mathematics from Cornell University, and is currently a Ph.D. candidate in the Java programming languages team at Rice University. His research concerns the development of semantic models and static analysis tools for 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. Contact Eric at eallen@cs.rice.edu.
Comments (Undergoing maintenance)





