ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Generics and Method Objects
Pages: 1, 2, 3, 4

Another variation of this problem arises when you try to write an equals method. Object defines equals with the following signature:



        public boolean equals(Object object)

Rather than implementing a strongly-typed version of this, it's often good practice to correctly implement this signature, because you cannot guarantee that the caller knows the type of the object it is passing to equals. If the caller doesn't know the type of the object it is passing in, then it might call the equals method with the signature equals(Object object), and you might wind up with a incorrect negative return value. So most programmers end up writing something like the following:

        public boolean equals(Object object) {
            // if the other instance doesn't have the same class,
            // they're not equal            
            if (object.getClass() != this.getClass()) {
                return false;
            }
            // okay. it's the same class. let's cast it and
            // *really* check for equality.
        }
    }

But this way of thinking can cause problems. Consider the following class:

    public class Athlete<T extends Sport> {
        private int _number;
        private String _team;

        // ....
        public boolean equals(Object object) {
            if (object.getClass() != this.getClass()) {
                return false;
            }
        // If they have the same number and are on the same team,
        // they're the same person
            Athlete otherAthlete = (Athlete) object;
            if (_number != otherAthlete.getNumber()) {
                return false
            }
            if (_team != otherAthlete.getTeam()) {
                return false
            }
            return true;
        }
    }

In this admittedly contrived example, we have the Athlete class defined as a generic class with a single type variable named T. And the equals method has a certain face validity, until you realize that getClass returns Athlete, not Athlete<Football> or Athelete<Baseball>. As it stands, this code would claim that the football player Will Allen (number 25 on the Giants) is the same as the baseball player Barry Bonds (number 25 on the Giants). Intuitively, I expect this problem to be eliminated by the first if, which checks the classes.

The moral of the story is: be careful (or be prepared to defend the claim that Will Allen is the best slugger since Babe Ruth). In this case, I'd probably define FootballPlayer and BaseballPlayer to be explicit subclasses of Athlete

Generics and Inheritance

Another interesting question revolves around how generics works with inheritance. Recall that, in part 2 of this series, we had a whole hierarchy of command objects. In particular, we had an abstract base class, AbstractRemoteMethodCall; an abstract subclass, ServerDescriptionBasedRemoteMethodCall; and a concrete subclass TranslateWord.

In general, subclasses rarely add new type variables. Type variables represent extra flexibility in how a class can be used, and abstract superclasses are "more generic" than concrete subclasses. Put more concretely, by the time we get around to writing a very specific class like TranslateWord, any flexibility as to what the argument or return value types can be ought to be gone. This is summarized in the following UML diagram:

Diagram.
The relationship between inheritance and genericity in our command object framework. Generally speaking, subclasses are more constrained (and less generic) than superclasses.

In particular, we're going to make AbstractRemoteMethodCall's makeCall method generic, so that both the return value and the exceptions thrown from makeCall can be strongly typed. Here's what the class and method declarations will look like for all three classes.

    public abstract class AbstractRemoteMethodCall<T, E extends Exception> {
        public T makeCall() throws ServerUnavailable, E {
            // ...
        }
        protected abstract T performRemoteCall(Remote remoteObject) throws RemoteException, E;
        // ...
    }

    public abstract class ServerDescriptionBasedRemoteMethodCall<T, E extends Exception > extends
        AbstractRemoteMethodCall<T, E>{
    //    ...
    //    no declaration for performRemoteCall; it's left abstract.
    }

    public class TranslateWord extends
        ServerDescriptionBasedRemoteMethodCall<Word, CouldNotTranslateException>  {
    //    ...
        protected abstract Word performRemoteCall(Remote remoteObject) throws RemoteException;
    }

Note what we did. When we defined ServerDescriptionBasedRemoteMethodCall<T, E >, we simply declared it to extend AbstractRemoteMethodCall<T, E>. This means that when the parametrized types for an instance of ServerDescription are specified, they will be used by the code for AbstractRemoteMethodCall.

There's a very important catch here -- this relies on the order of the type variables, and not their names. The following code, which switches the order of arguments and sets the superclasses' T type variable to the value of the subclass's W type variable, is perfectly valid (if somewhat confusing):

    public class Superclass<T>{
    // ...
    }
    public class Subclass<T,W> extends Superclass<W> {
    // ...
    }

The other way to deal with generic types within inheritance is to simply specify the concrete types directly, as part of the subclass declaration. We did this with TranslateWord, when we wrote extends ServerDescriptionBasedRemoteMethodCall<Word, CouldNotTranslateException>. This line of code automatically tells the compiler "Whenever someone creates an instance of TranslateWord, T is mapped to Word and E is mapped to Exception."

This is all pretty straightforward; the important point to note is that you can't deal with the problem dynamically, or at runtime. Code like the following, which attempts to set the type variable from within a constructor, just won't work:

    public class TranslateWord extends ServerDescriptionBasedRemoteMethodCall<T> {
        public TranslateWord() {
            super<Word>();

When you think about it, this is reasonable -- the whole goal of the generics specification is to enable the compiler to check types at compile-time.

Generics and Exceptions

The last remaining topic I'm going to cover in this whirlwind tour of JSR-014 is how to use generic exceptions. By now, you should have some intuition about how they're going to work: the class definition will include a parametrized type for an exception and the method definition will simply declare that it throws the type variable. In fact, we surreptitiously did exactly that in the previous section. Here's the example code:

    public abstract class AbstractRemoteMethodCall<T, E>{
        public T makeCall() throws ServerUnavailable, E {
            // ...
        }
        //...
    }

In this code snippet, AbstractRemoteMethodCall is declared to take two type variables, T and E. T is used as the return value on makeCall and E is used for exceptions. This works, and makes the code a little bit more typesafe (recall that, previously, makeCall was declared to throw Exception).

To be honest, though, I'm a little disappointed in the way exceptions are handled. The basic problem is this: while you can throw generic exceptions, you can't specify a different number of exceptions in the subclass. I'd love to have some notion of variable length generic types for exceptions so that something like the following code would mean "where the superclass claims to throw E, the subclass wants the compiler to interpret E as being one of these four types (all of which must be caught over on the client side)":

    public abstract class AbstractRemoteMethodCall<T, E>{
        public T makeCall() throws ServerUnavailable, E {
            // ...
        }
        //...
    }

    public abstract class Subclass<T>extends  
        AbstractRemoteMethodCall<T, oneof A&B&C&D> {
    
        public T makeCall() throws ServerUnavailable, E {

As matters stand, the subclass must declare that it throws some common superclass of A, B, C, and D. But that's a minor quibble. The ability to narrow exceptions in frameworks and libraries is extraordinarily useful. To see why, consider a method that, because it will be implemented by a variety of subclasses, is declared as throwing Exception. For example,

        public T performComputation(W argument) throws Exception {

The problem with this is that the code inside performComputation isn't forced to catch any exceptions. It doesn't even have to catch the exceptions that are local problems (e.g. exceptions that the superclass doesn't know how to handle or correct).

Suppose, for example, one implementation of performComputation contained some JDBC calls. Ordinarily, JDBC code has to be wrapped in a try/catch block to catch instances of SQLException. SQLException is a checked exception and the compiler will make sure that it, or a superclass, is caught somewhere in the stack frame. Which means that if performComputation wasn't declared as throwing Exception, the method wouldn't even compile until the possibility of an SQLException was explicitly addressed.

But since performComputation is declared to throw Exception, the compiler won't complain about SQLException. Instead, when an instance of SQLException is thrown, will be propagated up the stack, to eventually be caught someplace else.

On the other hand, consider the following declaration:

        public T performComputation(W argument) throws E {

In this method, any exceptions that don't extend E must still be checked locally. This means that the compiler will force you to deal with some exceptional conditions locally, which is what you probably want. In the case of JDBC calls, as long as E isn't a superclass of SQLException, the implementation of until performComputation will have to address the possibility of an SQLException.

Note:. This leads to an important design tip -- always use a separate exception hierarchy for exceptions in your program. If you throw generic exceptions, or overload ones that are defined by frameworks you happen to be using, you're working against the compiler.

Adding Generic Arguments and Return Values to the Command Object Framework

If you've read this far, and especially if you read the previous section, you're probably thinking something like "Ouch! My head feels like it's been put through a meat-grinder! What is the point of all this stuff?" Well, remember the goal of the generics specification I stated a few pages back:

By using generics, a programmer can convert casting errors from run-time exceptions to compiler errors. This results in fewer programming errors and better code.

In this section, we're going to systematically convert the command objects framework into code that uses generics. The first class we're going to convert is RemoteStubCache.

Recall that RemoteStubCache is a local cache of stubs used by an RMI client. Unless stubs are cached locally, an RMI client has to make two remote method calls for every "logical remote call": one remote call to a naming service and one remote call to the actual remote server. Converting RemoteStubCache is pretty easy: the only thing that must be changed is the internal use of Hashtable. In fact, only two lines of code changed. Here's the new version of RemoteStubCache:

    public class RemoteStubCache
    {
        // this next declaration used to be
        //    private static Hashtable _serverDescriptionsToStubs...

        private static Hashtable<ServerDescription, RemoteStub>_serverDescriptionsToStubs= new Hashtable<ServerDescription, RemoteStub>();

        public static RemoteStub getStubToRemoteObject(ServerDescription serverDescription) throws ServerUnavailable {
        // no cast necessary when pulling values from the hashtable
            RemoteStub returnValue = _serverDescriptionsToStubs.get(serverDescription);
            if (null == returnValue) {
                returnValue = serverDescription.getStub() ;
                if (null!= returnValue) {
                    _serverDescriptionsToStubs.put(serverDescription, returnValue);
                }
                else {
                    throw new ServerUnavailable();
                }
            }
            return returnValue;
        }
        public static void noLongerUsingStubToRemoteObject(ServerDescription serverDescription) {
        /*
            Needs to be implemented if we take cleaning up the cache
            seriously.
        */
        }
    
        public static void removeStubFromCache(ServerDescription serverDescription)  {
            _serverDescriptionsToStubs.remove(serverDescription);
        }
    }

AbstractRemoteMethodCall is trickier. We're going to add two type variables. The first type variable will be used for the value returned from from makeCall and the second will restrict the range of allowable exceptions. Here's the code:

    public abstract class AbstractRemoteMethodCall<T, E extends Exception> {
        public T makeCall() throws ServerUnavailable, E {
            RetryStrategy strategy = getRetryStrategy();
            while (strategy.shouldRetry()) {
                Remote remoteObject = getRemoteObject();
                if (null==remoteObject) {
                    throw new ServerUnavailable();
                }
                try {
                    return performRemoteCall(remoteObject);
                }
                catch (RemoteException remoteException) {
                    try {
                        remoteExceptionOccurred(remoteException);
                        strategy.remoteExceptionOccurred();
                    }
                    catch (RetryException retryException) {
                        handleRetryException(remoteObject);
                    }
                }
            }        
            return null;
        }
    
        protected abstract Remote getRemoteObject() throws ServerUnavailable;

        protected abstract T performRemoteCall(Remote remoteObject) throws RemoteException, E;

        protected RetryStrategy getRetryStrategy() {
            return new AdditiveWaitRetryStrategy();
        }

         protected void remoteExceptionOccurred(RemoteException remoteException) {
            /* ignored in based class. */
        }

        protected void handleRetryException(Remote remoteObject) throws ServerUnavailable {
            ExceptionLog.reportException("Repeated attempts to communicate with " + remoteObject + " failed.");
            throw new ServerUnavailable();
        }
    }

The point of this is the change in the definition of the abstract method performRemoteCall. In the previous two articles in this series, makeCall and performRemoteCall were defined as follows:

        public Object makeCall() throws ServerUnavailable, Exception {
            // Literally the exact same code...
        }
        protected abstract Object performRemoteCall(Remote remoteObject) throws RemoteException, Exception;

The new version defines them using two type variables, T and E. Concrete subclasses can stipulate the value of T and E, as in the following declaration of TranslateWord:

    public class TranslateWord extends ServerDescriptionBasedRemoteMethodCall<Word, CouldNotTranslateException>>  {
        private Word _sourceWord;
        private Language _targetLanguage;
        public TranslateWord(ServerDescription serverDescription, Word sourceWord, Language targetLanguage) {
            super(serverDescription);
            _sourceWord = sourceWord;
            _targetLanguage = targetLanguage;
        }

        // note how return type of performRemoteCall matches template variable.
        // it also matches the return type defined in the Translator interface
        protected Word performRemoteCall(Remote remoteObject) throws RemoteException, CouldNotTranslateException {
            Translator translator = (Translator) remoteObject;
            return translator.translate(_sourceWord, _targetLanguage);
        }
    }

The result of this is that code in ClientFrame that uses the TranslateWord command object to perform a remote method invocation doesn't need to cast the return value from makeCall. Here's the new code:

            TranslateWord translateMethod = new TranslateWord(translatorServerDescription, word, targetLanguage);
            try {
                // no cast in next line
                Word result = translateMethod.makeCall();

Pages: 1, 2, 3, 4

Next Pagearrow