Effective Generics
8.4 Maintain Binary Compatibility
Other Types Specialization at other types works similarly. For example, replacing String by Integer in Example 8-1 gives an interface ListInteger and classes ListInteg ers and ArrayListInteger. This even works for lists of lists. For example, replacing String by ListString in Example 8-1 gives an interface ListListString and classes ListListStrings and ArrayListListString.
However, specialization at wildcard types can be problematic. Say we wanted to spe- cialize both of the types List<Number> and List<? extends Number>. We might expect to use the following declarations:
// illegal
interface ListNumber extends List<Number>, ListExtendsNumber {}
interface ListExtendsNumber extends List<? extends Number> {}
This falls foul of two problems: the first interface extends two different interfaces with the same erasure, which is not allowed (see Section 4.4), and the second interface has a supertype with a wildcard at the top level, which is also not allowed (see Sec- tion 2.8). The only workaround is to avoid specialization of types containing wildcards;
fortunately, this should rarely be a problem.
// generic version -- breaks binary compatibility public static <T extends Comparable<? super T>>
T max(Collection<? extends T> coll)
But this signature has the wrong erasure—its 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, Com parable provides a good example.
Most legacy core classes that implement Comparable provide two overloads of the com pareTo 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 compar eTo(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 compar eTo(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 Example 8-2 and Example 8-3.
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 leg- acy versions of both SimpleName and ExtendedName would be required to implement compareTo(Name) and the problem described here could not arise.
8.4 Maintain Binary Compatibility | 119
Covariant Overriding Another corner case arises in connection with covariant over- riding (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() { ... } ...
}
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
} }
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!
} }
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.
8.4 Maintain Binary Compatibility | 121