Generics and Method Objects

Abstract

In the first article in this series, I introduced a distributed translation service and showed how using a simple command object framework to encapsulate retry logic simultaneously simplified the client application and made it more robust. The second article in this series built on this framework to implement a caching system for stubs to remote servers.

In this article, I'll introduce you to the new Generics Specification (which came out of the Java Community Process) and then rebuild the command object framework using it. This won't actually change the performance of the application or add any new functionality. It will, however, add a substantial amount of compile-time type checking to the framework, thereby making the code easier to maintain and extend.

Most of this article is concerned with generic classes and generic methods (hereafter simply generics). As with the other articles in this series, this article requires a fair amount of RMI knowledge and experience; if you're not familiar with RMI, my book Java RMI is a great place to start. In addition, this article also requires a little bit of patience -- generics aren't particularly difficult, but fully understanding them does take time.

Frameworks, Libraries, and Type Safety

By any count, the number of libraries available for the Java programmer is enormous. The last time I checked the JDK, it clocked in at well over 4,000 classes. The various Apache projects have at least that many classes, as well. And then there're the thousands of projects at SourceForge, and the whole Giant Java Tree and the Hypersonic database and the JBoss application server and... you get the point. There's a lot of code out there, and a lot of it is intended to be reused across multiple programs. This is a wonderful state of affairs. Having vast repositories of publicly available code -- which has been tested and retested and used and reused -- makes life easier.

But because they're intended for use across multiple projects, the frameworks and libraries out there have a significant design flaw: they're not very type safe. In fact, method signatures in most libraries are deliberately written to use what I call the weakest plausible type.

The weakest plausible type is simply the common superclass of all the plausible argument types. That is, it's the common superclass of all the classes that can be reasonably be used as arguments to a method. Consider, for example, the collection classes defined in the java.util package. A collection is a container; it holds objects. Since you can imagine storing just about any type of object in a container, Object is the weakest plausible type for most of the method signatures. And, hence, the signatures for Collection were defined using Object, as in the following two methods from the Collection interface:

boolean add(Object o)
boolean contains(Object o)

Having signatures like these makes the Collections library reusable across projects. It doesn't matter than I'm writing an accounting package and you're working on a module for a distributed gaming system -- we can both store our objects in instances of Vector. But this flexibility makes the Collections library more dangerous to use in any given project. When an instance is retrieved from a collection, whether directly or via an Iterator, it has a declared type of Object. Since Object is rarely sufficient, every single programmer who uses the Collections classes winds up writing code like the following:

This is a significant problem in Java. To the extent that a programmer is forced to use casts on return values from methods, there is the possibility of the Java runtime throwing unexpected instances of ClassCastException. And, since ClassCastException is an unchecked exception, it's very easy for an error in casting an object to cause a catastrophic error in your code.

Also in this series:

Learning Command Objects and RMI -- O'Reilly's Java RMI author William Grosso introduces you to the basic
ideas behind command objects by providing a translation
service from a remote server and using command objects
to structure the RMI made from a client program.

Seamlessly Caching Stubs for Improved Performance -- In Part 2 of this RMI series, William Grosso addresses a common problem with RMI apps -- too many remote method calls to a naming service. In this article he extends the framework introduced in Part 1 to provide seamless caching of stubs.

How do you deal with this? Up until now, programmers only had three options:

Be very careful to check all class casts by using instanceof before every cast. Note that this is entirely a matter of programmer discipline -- the java compiler won't help you with this or point out if you've forgotten to check instanceof in a particular case.

Be very careful to catch ClassCastException and handle the exception correctly wherever it could be thrown. Note that this too is entirely a matter of programmer discipline -- the java compiler doesn't warn you if you don't catch ClassCastException.

Hope that all the casts are correct.

In practice, almost everyone chooses the third option (relying on QA to catch all the mistakes before the application ships). In fact, even in the code for previous articles in this series, we've seen code that blithely casts return values to the "correct" type. For example, the signature of AbstractRemoteMethodCall's makeCall is the following:

public Object makeCall() throws ServerUnavailable, Exception

Classes that call this method have to cast the return value correctly, as in the following lines from TranslatorPanel:

This example is especially egregious because a few lines later, inside the setContents method of model, the elements of the Vector are cast as instances of String. That is, the code that called getRegistryContents simply knew that the return value -- even though it was declared to be an instance of Object -- was an instance of Vector. And, furthermore, it knew that the Vector only contained instances of String, even though Vector is declared to contain instances of Object. It turns out that the code for this article is correct, but wouldn't it be nice if the compiler was able to verify it?

And that's the goal of the generics specification. By using generics, a programmer can convert instances of ClassCastException thrown at run-time errors into compile-time errors. This results in fewer programming errors and better code.

Using generics also saves time. If types are explicitly declared, then it's easy for programmers to figure out the return type of a method, or to determine the types of objects stored in a Vector. If generics aren't used, the alternative is a frustrating rummage through the codebase, looking for a helpful comment or two (something like //This vector can only contain instances of Contractor).