Section 8.4. Maintain Binary Compatibility


8.4. Maintain Binary Compatibility

As we have stressed, generics are implemented via erasure in order to ease evolution. When evolving legacy code to generic code, we want to ensure that the newly-generified code will work with any existing code, including class files for which we do not have the source. When this is the case, we say that the legacy and generic versions are binary compatible.

Binary compatibility is guaranteed if the erasure of the signature of the generic code is identical to the signature of the legacy code and if both versions compile to the same bytecode. Usually, this is a natural consequence of generification, but in this section we look at some of the corner cases that can cause problems.

Some examples for this section were taken from internal Sun notes written by Mark Reinhold.

Adjusting the Erasure One corner case arises in connection with the generification of the max method in the Collections class. We discussed this case in Sections 3.2 and 3.6, but it is worth a quick review.

Here is the legacy signature of this method:

 // legacy version public static Object max(Collection coll) 

And here is the natural generic signature, using wildcards to obtain maximum flexibility (see Section 3.2):

 // generic version -- breaks binary compatibility public static <T extends Comparable<? super T>>   T max(Collection<? extends T> coll) 

But this signature has the wrong erasureits return type is Comparable rather than Object. In order to get the right signature, we need to fiddle with the bounds on the type parameter, using multiple bounds (see Section 3.6). Here is the corrected version:

 // generic version -- maintains binary compatibility public static <T extends Object & Comparable<? super T>>   T max(Collection<? extends T> coll) 

When there are multiple bounds, the leftmost bound is taken for the erasure. So the erasure of T is now Object, giving the result type we require.

Some problems with generification arise because the original legacy code contains less-specific types than it might have. For example, the legacy version of max might have been given the return type Comparable, which is more specific than Object, and then there would have been no need to adjust the type using multiple bounds.

Bridges Another important corner case arises in connection with bridges. Again,\break Comparable provides a good example.

Most legacy core classes that implement Comparable provide two overloads of the compareTo method: one with the argument type Object, which overrides the compareTo method in the interface; and one with a more-specific type. For example, here is the relevant part of the legacy version of Integer:

 // legacy version public class Integer implements Comparable {   public int compareTo(Object o) { ... }   public int compareTo(Integer i) { ... }   ... } 

And here is the corresponding generic version:

 // generic version -- maintains binary compatibility    public final class     Integer implements Comparable<Integer> {   public int compareTo(Integer i) { ... }   ... } 

Both versions have the same bytecode, because the compiler generates a bridge method for compareTo with an argument of type Object (see Section 3.7).

However, some legacy code contains only the Object method. (Previous to generics, some programmers thought this was cleaner than defining two methods.) Here is the legacy version of javax.naming.Name.

 // legacy version public interface Name extends Comparable {   public int compareTo(Object o);   ... } 

In fact, names are compared only with other names, so we might hope for the following generic version:

 // generic version -- breaks binary compatibility public interface Name extends Comparable<Name> {   public int compareTo(Name n);   ... } 

However, choosing this generification breaks binary compatibility. Since the legacy class contains compareTo(Object) but not compareTo(Name), it is quite possible that users may have declared implementations of Name that provide the former but not the latter. Any such class would not work with the generic version of Name given above. The only solution is to choose a less-ambitious generification:

 // generic version -- maintains binary compatibility public interface Name extends Comparable<Object> {   public int compareTo(Object o) { ... }   ... } 

This has the same erasure as the legacy version and is guaranteed to be compatible with any subclass that the user may have defined.

In the preceding case, if the more-ambitious generification is chosen, then an error will be raised at run time, because the implementing class does not implement compareTo(Name). But in some cases the difference can be insidious: rather than raising an error, a different value may be returned! For instance, Name may be implemented by a class SimpleName, where a simple name consists of a single string, base, and comparing two simple names compares the base names. Further, say that SimpleName has a subclass ExtendedName, where an extended name has a base string and an extension. Comparing an extended name with a simple name compares only the base names, while comparing an extended name with another extended name compares the bases and, if they are equal, then compares the extensions. Say that we generify Name and SimpleName so that they define compareTo(Name), but that we do not have the source for ExtendedName. Since it defines only compareTo(Object), client code that calls compareTo(Name) rather than compareTo(Object) will invoke the method on SimpleName (where it is defined) rather than ExtendedName (where it is not defined), so the base names will be compared but the extensions ignored. This is illustrated in Examples 8.2 and 8.3.

Example 8-2. Legacy code for simple and extended names

 interface Name extends Comparable {   public int compareTo(Object o); } class SimpleName implements Name {   private String base;   public SimpleName(String base) {     this.base = base;   }   public int compareTo(Object o) {     return base.compareTo(((SimpleName)o).base);   } } class ExtendedName extends SimpleName {   private String ext;   public ExtendedName(String base, String ext) {     super(base); this.ext = ext;   }   public int compareTo(Object o) {     int c = super.compareTo(o);     if (c == 0 && o instanceof ExtendedName)       return ext.compareTo(((ExtendedName)o).ext);     else       return c;   } } class Client {   public static void main(String[] args) {     Name m = new ExtendedName("a","b");     Name n = new ExtendedName("a","c");     assert m.compareTo(n) < 0;   } } 

Example 8-3. Generifying simple names and the client, but not extended names

 interface Name extends Comparable<Name> {   public int compareTo(Name o); } class SimpleName implements Name {   private String base;   public SimpleName(String base) {     this.base = base;   }   public int compareTo(Name o) {     return base.compareTo(((SimpleName)o).base);   } } // use legacy class file for ExtendedName class Test {   public static void main(String[] args) {     Name m = new ExtendedName("a","b");     Name n = new ExtendedName("a","c");     assert m.compareTo(n) == 0;  // answer is now different!   } } 

The lesson is that extra caution is in order whenever generifying a class, unless you are confident that you can compatibly generify all subclasses as well. Note that you have more leeway if generifying a class declared as final, since it cannot have subclasses.

Also note that if the original Name interface declared not only the general overload compareTo(Object), but also the more-specific overload compareTo(Name), then the legacy versions of both SimpleName and ExtendedName would be required to implement compareTo(Name) and the problem described here could not arise.

Covariant Overriding Another corner case arises in connection with covariant overriding (see Section 3.8). Recall that one method can override another if the arguments match exactly but the return type of the overriding method is a subtype of the return type of the other method.

An application of this is to the clone method:

 class Object {   public Object clone() { ... }   ... } 

Here is the legacy version of the class HashSet:

 // legacy version class HashSet {   public Object clone() { ... }   ... } 

For the generic version, you might hope to exploit covariant overriding and choose a more-specific return type for clone:

 // generic version -- breaks binary compatibility class HashSet {   public HashSet clone() { ... }   ... } 

However, choosing this generification breaks binary compatibility. It is quite possible that users may have defined subclasses of HashSet that override clone. Any such subclass would not work with the generic version of HashSet given previously. The only solution is to choose a less-ambitious generification:

 // generic version -- maintains binary compatibility class HashSet {   public Object clone() { ... }   ... } 

This is guaranteed to be compatible with any subclass that the user may have defined. Again, you have more freedom if you can also generify any subclasses, or if the class is final.




Java Generics and Collections
Java Generics and Collections
ISBN: 0596527756
EAN: 2147483647
Year: 2006
Pages: 136

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net