The idea is to parameterize the key instead of the container.
consider a
The
When a class literal is passed among methods to communicate both compile-time and runtime type information, it is called a type token [Bracha04].
// Typesafe heterogeneous container pattern - API
public class Favorites {
public <T> void putFavorite(Class<T> type, T instance);
public <T> T getFavorite(Class<T> type);
}
// 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());
}
consider a
Favorites
class that allows its clients to store and retrieve a favorite instance of arbitrarily many types. The
Class
object for the type will play the part of the parameterized key. The reason this works is that class Class
is generic. The type of a class literal is not 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 [Bracha04].
// Typesafe heterogeneous container pattern - API
public class Favorites {
public <T> void putFavorite(Class<T> type, T instance);
public <T> T getFavorite(Class<T> type);
}
// 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());
}
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<>();
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}
public <T> T getFavorite(Class<T> type) {
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
}
}
This means that every key can have a differentparameterized type: one can be
Class<String>
, the next Class<Integer>
, and so on. That’s where the heterogeneity comes from.
public class Class<T> {
T cast(Object obj);
}
T cast(Object obj);
}
// Achieving runtime type safety with a dynamic cast
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(type, type.cast(instance));
}
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 static factories
The second limitation of the
Favorites
class is that it cannot be used on a non-reifiable type (Item 28). In other words, you can store your favorite String
or String[]
, but not your favorite List<String>
. your program won’t compile. The reason is that you can’t get a Class
object for List<String>
. The class literal 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 this limitation.
The type tokens used by
Favorites
are unbounded: getFavorite
and put-Favorite
accept any Class
object. Sometimes you may need to limit the types that can be passed to a method. This can be achieved with a bounded type token, which is simply a type token that places a bound on what type can be represented, using a bounded type parameter (Item 30) or a bounded wildcard (Item 31).
ublic <T extends Annotation>
T getAnnotation(Class<T> annotationType);
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 27). 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 is 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));
}
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.
No comments:
Post a Comment