If you decide to convert old code to use generics, you need to think carefully about how you modify the API.
You need to make certain that the generic API is not unduly restrictive;
it must continue to support the original contract of the API.
Consider again some examples from java.util.Collection
. The pre-generic
API looks like:
interface Collection { public boolean containsAll(Collection c); public boolean addAll(Collection c); }
interface Collection<E> { public boolean containsAll(Collection<E> c); public boolean addAll(Collection<E> c); }
containsAll()
method works with any kind of
incoming collection. It will only succeed if the incoming collection
really contains only instances of E
, but:
Collection<S>
,where S
is a
subtype of
E
.
containsAll()
with a
collection of a different type. The routine should work, returning
false
.
addAll()
, we should be able to add any collection
that consists of instances of a subtype of E
. We saw how to
handle this situation correctly in section
Generic Methods.
You also need to ensure that the revised
API retains binary compatibility with old clients. This implies that the
erasure of the API must be the same as the original, ungenerified API. In most
cases, this falls out naturally, but there are some subtle cases. We'll
examine one of the subtlest cases we've encountered,
the method Collections.max()
.
As we saw in section
More Fun with Wildcards, a plausible signature for max()
is:
public static <T extends Comparable<? super T>> T max(Collection<T> coll)
public static Comparable max(Collection coll)
max()
:
public static Object max(Collection coll)
max()
, but it was not
done, and all the old binary class files that call Collections.max()
depend on a signature that returns Object
.
We can force the erasure to be different, by explicitly specifying a
superclass in the bound for the formal type parameter T
.
public static <T extends Object & Comparable<? super T>> T max(Collection<T> coll)
T1 & T2 ... & Tn
. A type variable with multiple bounds is known to be a subtype of all
of the types listed in the bound. When a multiple bound is used, the first type mentioned in
the bound is used as the erasure of the type variable.
Finally, we should recall that max
only reads from its input
collection, and so is applicable to collections of any subtype of T
.
This brings us to the actual signature used in the JDK:
public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)
Another issue to watch out for is covariant returns, that is, refining the return type of a method in a subclass. You should not take advantage of this feature in an old API. To see why, let's look at an example.
Assume your original API was of the form:
public class Foo { public Foo create() { ... } // Factory. Should create an instance of whatever class it is declared in. } public class Bar extends Foo { public Foo create() { ... } // Actually creates a Bar. }
public class Foo { public Foo create() { ... } // Factory. Should create an instance of whatever class it is declared in. } public class Bar extends Foo { public Bar create() { ... } // Actually creates a Bar. }
public class Baz extends Bar { public Foo create() { ... } // Actually creates a Baz. }
Baz
is recompiled,
it will not properly override the create()
method of
Bar
. Furthermore, Baz
will
have to be modified, since the code will be rejected as written--the
return type of create()
in Baz
is not a subtype of the return type of create()
in Bar
.