第29条:优先考虑类型安全的异构容器
泛型最常用于集合,如Set
和Map
,以及单元素的容器。如ThreadLocal
和AtomicReference
。在这些用法中,它都充当被参数化了的容器。这样就限制你每个容器只能有固定数目的类型参数。一般来说,这种情况正是你想要的。一个Set
只有一个类型参数,表示它的元素类型;一个Map
有两个类型参数,表示它的键和值类型;诸如此类。
但是,有时候你会需要更多的灵活性。例如,数据库行可以有任意多的列,如果能以类型安全的方式访问所有列就好了。幸运的是,有一种方法可以很容易地做到这一点。这种想法就是将键(key)进行参数化而不是将容器(container)参数化。然后将参数化的键提交给容器,来插入或者获取值。用泛型系统来确保值的类型与它的键相符。
简单地示范一下这种方法:考虑Favorites
类,它允许客户端从任意数量的其他类中,保存并获取一个“最喜爱”的实例。Class
对象充当参数化键的关键部分。之所以可以这样,是因为类Class
在 Java 1.5 版本中被泛型化了。类的类型从字面上来看不再只是简单的Class
,而是Class<T>
。例如,String.class
属于Class<String>
类型,Integer.class
属于Class<Integer>
类型。当一个类的字面文字被用在方法中,来传达编译时和运行时的类型信息是,就被称作type token[Brancha04]。
Favorites
类的API很简单。它看起来就像一个简单的map,除了键(而不是map)被参数化之外。客户端在设置和获取最喜爱的实例时提交Class
对象。下面就是这个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);
}
下面是一个示例程序,检验一下Favorites
类,它保存、获取并打印一个最喜爱的String
、Integer
和Class
实例:
// 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());
}
正如所料,这段程序打印出的是Java cafebabe Favorites
。
Favorites
实例是类型安全(typesafe)的:当你向它请求String
的时候,它从来不会返回一个Integer
给你。同时它也是异构的(heterogeneous):不像普通的map,它的所有键都是不同类型的。因此,我们将Favorites
称作类型安全的异构容器(typesafe heterogeneous container)。
Favorites
的实现小得出奇。它的完整实现如下:
// 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> getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
这里发生了一些微妙的事情。每个Favorites
实例都得到一个称作favorites
的私有Map<Class<?>, Object>
的支持。你可能认为由于无限通配符类型的关系,将不能把任何东西放进这个Map
中,但事实正好相反。要注意的是通配符类型是嵌套的:它不是属于通配符类型的Map
的类型,而是它的键的类型。由此可见,每个键都可以有一个不同的参数化类型:一个可以是Class<String>
,接下来是Class<Integer>
等等。异构就是从这里来的。
第二件要注意的事情是,favorites Map
的值类型只能是Object
。换句话说,Map
并不能保证键和值之间的类型关系,即不能保证每个值的类型都与键的类型相同。事实上,Java
的类型系统还没有强大到足以表达这一点。但我们知道这是事实,并在获取favorite
的时候利用了这一点。
putFavorite
方法的实现很简单:它只是把(从指定的Class
对象到指定的favorite
实例的)一个映射放到favorites
中。如前所述,这是放弃了键和值之间的“类型联系”,因此无法知道这个值是键的一个实例。但是没关系,因为getFavorite
方法能够并且的确重新建立了这种联系。
getFavorite
方法的实现比putFavorite
的更难一些。它先从favorites
映射中获得与指定Class
对象相对应的值。这正是要返回的对象引用,但它的编译时类型是错误的。它的类型只是Object
(favorites
映射的值类型),我们需要返回一个T
。因此,getFavorite
方法的实现利用Class
的cast
方法,将对象引用动态地转换(dynamically cast)成了Class
对象所表示的类型。
cast
方法是Java
的cast
操作符的动态模拟。它只检验它的参数是否为Class
对象所表示的类型的实例。如果是,就返回参数;否则就抛出ClassCastException
异常。我们知道,getFavorite
中的cast
调用永远也不会抛出ClassCastException
异常,并假设客户端代码正确无误地进行了编译。也就是说,我们知道favorites
映射中的值会始终与键的类型相匹配。
假设cast
方法只返回它的参数,那它能为我们做什么呢?cast
方法的签名充分利用了Class
类型被泛型化的这个事实。它的返回类型是Class
对象的类型参数:
public class Class<T> {
T cast(Object obj);
}
这正是getFavorite
方法所需要的,也正是让我们不必借助于未受检地转换成T
就能确保Favorites
类型安全的东西。
Favorites
类有两种局限性值得注意。首先,恶意的客户端可以很轻松地破坏Favorites
实例的类型安全,只要以它的原生态类型(ray form)使用Class
对象。但会造成客户端代码在编译时产生未受检的警告。这与一般的集合实现,如HashSet
和HashMap
并没有什么区别。也就是说,如果愿意付出一点点代价,就可以拥有运行时的类型安全。确保Favorites
永远不违背它的类型约束条件的方式是,让putFavorite
方法检验instance
是否真的是type
所表示的类型的实例。我们已经知道这要如何进行了,只要使用一个动态的转换:
// Achieving runtime type safty with a dynamic cast
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(type, type.cast(instance));
}
java.util.Collections
中有一些集合包装类采用了同样的技巧。它们称作checkedSet
、checkedList
、checkedMap
,诸如此类。除了一个集合(或者映射)之外,它们的静态工厂还采用一个(或者两个)Class
对象。静态工厂属于泛型方法,确保Class
对象和集合的编译时类型相匹配。包装类给它们所封装的集合增加了具体化。例如,如果有人视图将Coin
放进你的Collection<Stamp>
,包装类就会在运行时抛出ClassCastException
异常。用这些包装类在混油泛型和遗留代码的应用程序中追溯“谁把错误的类型元素添加到了集合中”很有帮助。
Favorites
类的第二种局限性在于它不能用在不可具体化的(non-reifiable)类型中(见第25条)。换句话说,你可以保存最喜爱的String
或者String[]
,但不能保存最喜爱的List<String>
。如果试图保存最喜爱的List<String>
,程序就不能进行编译.原因在于你无法为List<String>
获得一个Class
对象:List<String>.class
是个语法错误,这也是件好事。List<String>
和List<Integer>
共用一个Class
对象,即List.class
。如果从“字面(type literal)”上来看,List<String>.class
和List<Integer>.class
是合法的,并返回了相同的对象引用,就会破坏Favorites
对象的内部结构。
对于第二种局限性,还没有完全令人满意的解决办法。有一种方法称作 super type token,它在解决这一局限性方面做了很多努力,但是这种方法仍然有它自身的局限性[Gafter07]。
Favorites
使用的类型令牌(type token)是无限制的:getFavorite
和putFavorite
接受任何Class
对象。有时候,可能需要限制那些可以传给方法的类型。这可以通过有限制的类型令牌(bounded type token)来实现,它只是一个类型令牌,利用有限制类型参数(见第27条)或者有限制通配符(见第28条),来限制可以表示的类型。
注解API(见第35条)广泛利用了有限制的类型令牌。例如,这是一个在运行时读取注解的方法。这个方法来自AnnotatedElement
接口,它通过表示类、方法、域及其他程序元素的反射类型来实现:
public <T extends Annotation>
T getAnnotation(Class<T> annotationType);
参数annotationType
是一个表示注解类型的有限制的类型令牌。如果元素有这种类型的注解,该方法就将它返回,如果没有,则返回null
。被注解的元素本质上是个类型安全的异构容器,容器的键属于注解类型。
假设你有一个类型Class<?>
的对象,并且想将它传递给一个需要有限制的类型令牌的方法,例如getAnnotation
。你可以将对象转化成Class<? extends Annotation>
,但是这种转换时非受检的,因此会产生一条编译时警告(见第24条)。幸运的是,类Class
提供了一个安全(且动态)地执行这种转换的实例方法。该方法称作asSubclass
,它将调用它的Class
对象转换成用其他参数表示的类的一个子类。如果转换成功,该方法返回它的参数;如果失败,则抛出ClassCastException
异常。
以下示范了如何利用asSubclass
方法在编译时读取类型未知的注解。这个方法编译时没有出现错误或者警告:
// 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));
}
总而言之,集合API说明了泛型的一般用法,限制你每个容器只能有固定数目的类型参数。你可以通过将类型参数放在键上而不是容器上类避开这一限制。对于这种类型安全的异构容器,可以用Class
对象作为键。以这种方式使用的Class
对象称作类型令牌。你也可以使用定制的键类型。例如,用一个DatabaseRow
类型表示一个数据库行(容器),用泛型Column<T>
作为它的键。