The most
common use of generics is for collections, such as Set and Map, and single-
element containers, such as ThreadLocal and AtomicReference. In all of these uses, it is the container
that is parameterized. This limits you to a fixed number of type parameters per
container.
Sometimes,
however, you need more flexibility. For example, a database row can have
arbitrarily many columns, and it would be nice to be able to access all of them
in a typesafe manner. Luckily, there is an easy way to achieve this effect. The
idea is to parameterize the key instead of the
container. Then present the parameterized key to the
container to insert or retrieve a value. The generic type system is used to
guarantee that the type of the value agrees with its key.
As a simple
example of this approach, consider a Favorites
class
that allows its clients to store and
retrieve a “favorite” instance of arbitrarily many other classes. The Class object will play the part of the
parameterized key. The reason this works is that class Class was generified in release 1.5. The type of a
class literal is no longer simply Class, but Class<T>. For example, String.class
is
of type Class<String>, and Integer.class is of type Class<Integer>. When a class
literal is passed among methods to communicate both compile-time and runtime
type information, it is called a type token.
The API for
the Favorites class is
simple. It looks just like a simple map, except that the key is parameterized
instead of the map. The client presents a Class
object
when setting and getting favorites. Here is the API:
// Typesafe heterogeneous container pattern - API
public class Favorites {
public <T> void putFavorite(Class<T> type, T
instance);
public <T> T getFavorite(Class<T> type);
}
Here is a
sample program that exercises the Favorites
class,
storing, retrieving, and printing a favorite String, Integer, and Class
instance:
// Typesafe heterogeneous container pattern - client
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class);
String favoriteString = f.getFavorite(String.class);
int favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoriteClass =
f.getFavorite(Class.class);
System.out.printf("%s %x %s%n",
favoriteString,
favoriteInteger, favoriteClass.getName());
}
As you might
expect, this program prints Java cafebabe
Favorites.
A Favorites instance is typesafe: it will never return an Integer
when
you ask it for a String. It is also heterogeneous: unlike an ordinary map,
all the keys are of different types. Therefore, we call Favorites a typesafe
heterogeneous container.
The
implementation of Favorites is
surprisingly tiny. Here it is, in its entirety:
// Typesafe heterogeneous container pattern -
implementation
public class Favorites {
private Map<Class<?>,
Object> favorites =
new HashMap<Class<?>, Object>();
public <T> void putFavorite(Class<T> type, T
instance) {
if (type == null)
throw new NullPointerException("Type is
null");
favorites.put(type, instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
There are a
few subtle things going on here. Each Favorites
instance
is backed by a private Map<Class<?>, Object> called favorites. You might think that you couldn’t put
anything into this Map because of the unbounded wildcard
type, but the truth is quite the opposite. The thing to notice is that the
wildcard type is nested: it’s not the type of the Map that’s a wildcard type but the type of its
key. This means that every key can have a different parameterized
type: one can be Class<String>, the next Class<Integer>, and so on. That’s where the
heterogeneity comes from.
The next thing
to notice is that the value type of the favorites
Map is
simply Object. In other
words, the Map does not guarantee the type
relationship between keys and values, which is that every value is of the type
represented by its key. In fact, Java’s type system is not powerful enough to
express this. But we know that it’s true, and we take advantage of it when it
comes time to retrieve a favorite.
So what does
the cast method do for us, given that it
simply returns its argument? The signature of the cast method takes full advantage of the fact that
class Class has been
generified. Its return type is the type parameter of the Class object:
public class Class<T> {
T cast(Object obj);
}
There are two
limitations to the Favorites class that are
worth noting. First, a malicious client could easily corrupt the type safety of
a Favorites instance,
simply by using a Class object in its
raw form. But the resulting client code would generate an unchecked warning
when it was compiled. This is no different from the normal collection
implementations such as HashSet and HashMap. You can easily put a String into a HashSet<Integer>
by
using the raw type HashSet
(Item 23).You
can have runtime type safety if you’re willing to pay for it. The way to ensure
that Favorites never violates
its type invariant is to have the putFavorite
method
check that instance is indeed an
instance of the type represented by type. And we
already know how to do this. Just use a dynamic cast:
// Achieving runtime type safety with a dynamic cast
public <T> void putFavorite(Class<T> type, T
instance) {
favorites.put(type, type.cast(instance));
}
There are
collection wrappers in java.util.Collections that play the
same trick. They are called checkedSet, checkedList, checkedMap, and so
forth. Their static factories take a Class
object
(or two) in addition to a collection (or map).
The second
limitation of the Favorites class is that
it cannot be used on a non-reifiable type (Item 25). In other words, you can
store your favorite String or String[], but not your favorite List<String>. If you try to store your favorite List<String>, your program won’t compile. The reason is
that you can’t get a Class object for List<String>: List<String>.class
is
a syntax error, and it’s a good thing, too. List<String>
and
List<Integer> share a single
Class object, which
is List.class. It would
wreak havoc with the internals of a Favorites
object
if the “type literals” List<String>.class and List<Integer>.class were legal and returned the same
object reference.
There is no
entirely satisfactory workaround for the second limitation. There is a
technique called super type tokens that goes a
long way toward addressing the limitation, but this technique has limitations
of its own [Gafter07].
The
annotations API (Item 35) makes extensive use of bounded type tokens. For
example, here is the method to read an annotation at runtime. This method comes
from the AnnotatedElement interface,
which is implemented by the reflective types that represent classes, methods,
fields, and other program elements:
public <T extends Annotation>
T getAnnotation(Class<T> annotationType);
The argument annotationType is a bounded type token representing an
annotation type. The method returns the element’s annotation of that type, if
it has one, or null, if it doesn’t. In essence, an
annotated element is a typesafe heterogeneous container whose keys are
annotation types.
Suppose you
have an object of type Class<?> and you want
to pass it to a method that requires a bounded type token, such as getAnnotation. You could cast the object to Class<? extends Annotation>, but this
cast is unchecked, so it would generate a compile-time warning (Item 24).
Luckily, class Class provides an
instance method that performs this sort of cast safely (and dynamically). The
method is
called asSubclass, and it casts
the Class object on
which it’s called to represent a subclass of the class represented by its
argument. If the cast succeeds, the method returns its argument; if it fails,
it throws a ClassCastException.
Here’s how you
use the asSubclass method to read
an annotation whose type is unknown at compile time. This method compiles
without error or warning:
// Use of asSubclass to safely cast to a bounded
type token
static Annotation getAnnotation(AnnotatedElement
element,
String annotationTypeName) {
Class<?> annotationType = null; // Unbounded type token
try {
annotationType = Class.forName(annotationTypeName);
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
return element.getAnnotation(
annotationType.asSubclass(Annotation.class));
}
In summary,
the normal use of generics, exemplified by the collections APIs, restricts you
to a fixed number of type parameters per container. You can get around this
restriction by placing the type parameter on the key rather than the container.
You can use Class objects as
keys for such typesafe heterogeneous containers. A Class object used in this fashion is called a type
token. You can also use a custom key type. For example, you could have a DatabaseRow type representing a database row (the
container), and a generic type Column<T>
as
its key.
Reference: Effective Java 2nd Edition by Joshua Bloch