泛型的常见用途是用于集合,比如Set
然而有时候,你需要更多的灵活性。比如,一个数据库行可以拥有任意多的列,如果它有一个类型安全的方法来访问所有列那就太好了。幸运的是,有一个简单的方式来实现这个效果。这个想法是将key参数化,而不是将容器参数化。然后将容器显示参数化的key,以插入或检索value。泛型类型系统用于保证value的类型与其key一致。
作为这个方法的简单例子,考虑使用Favorites类允许它的客户端来存储和检索任意多类型的favorite实例。Class对象将扮演参数化key的角色。这样可以工作的原因是Class类是泛型。类文字的类型不是简单的Class而是Class
Favorites类的API是简单的。它看起来只是一个简单map,除了它的key是参数化的而不是map。客户端在设置和获取favorites时显示一个Class对象。这是它的API:
如下是一个简单的程序使用Favorites类,存储,检索和打印一个favorite String,Integer,Class实例:
正如你所期望的,这个程序打印了“Java cafebabe Favorites”.请注意,顺便提一下,Java的printf方法和C的不同,你应该使用%n而在C中使用的是\n。%n生成适用于特定平台的行分隔符,该分隔符在许多但不是所有的平台上都是\n。
一个Favorites实例是类型安全的:当你问它要一个String的时候它永远不会返回一个Integer。它也是异构的:不像一个原始map。所有key都是不同类型的。所以,我们叫Favorites是类型安全的异构容器。
Favorites的实现是令人惊讶的瘦小,这是它的完整实现:
这里发生了一些微妙的事情。每个Favorites实例都基于一个叫做favorites的私有的Map
接下来要注意的是,Favorites map的值类型只是一个对象。换句话说,Map并不保证key和value之间的类型关系,即每个value都是由其key表示的类型。事实上,Java的类型系统并不足够强大来表达这一点。但是我们知道这是真的,我们利用它来检索favorite。
putFavorite实现是简单的:它简单地将一个从给定的Class对象到给定的favorite实例的映射放到favorites中。如前所述,这放弃了key和value的“类型链接”;它失去了该value是key的一个实例的知识。但这没关系,因为getFavorites方法可以重新建立这个关系。
getFavorite的实现比putFavorite复杂。首先,它从favorites map中获取对应于给定Class对象的值。这时应该返回正确的对象引用,但是它的编译时类型是错误的:他是Object(favorites map的value的类型),我们需要返回一个T。所以,getFavorite动态地通过Class对象代表的类型实现了强制类型转换对象,使用了Class的强制转换方法。
强转方法是与Java的强转操作符的动态模拟。它只是检查它的参数是否是Class对象表示的类型的实例。如果是,它返回参数;否则会抛出ClassCastException。我们知道getFavorite中的强制转换调用不会抛出ClassCastException,假设客户端代码编译干净。也就是说,我们知道favorites map的value总与它们的key的类型相匹配。
那么cast方法为我们做了什么呢,它只是简单地返回了它的参数吗?cast方法的签名充分利用了类是泛型的事实。它的返回类型是Class对象的类型参数:
这正是getFavorite方法所需要的。这就允许我们在不依赖于T的未检查的强转的情况下使得Favorites类型安全。
Favorites类有两个值得注意的限制。首先,一个有恶意的客户端会通过使用原始类型的Class对象来容易地破坏favorites实例的类型安全性。但是,生成的客户端代码在编译时会生成未经检查的警告。这与像HashSet和HashMap这样的正常集合实现没有什么不同。你可以很容易地将一个String放入HashSet
java.util.Collections中有一些集合包装器,他们可以玩同样的把戏。它们叫做checkedSet,checkedList,checkedMap诸如此类。它们的静态工厂除了集合(或map)还接受一个Class对象(或两个)。静态工厂是泛型方法,确保Class对象和集合在编译时类型匹配。包装器为它们包装的集合添加具体化。比如,如果有人尝试将Coin放入你的Collection
Favorites类的第二个限制是它不能用于不可还原的类型(item28)。换句话说,你可以将String或者String[]存储到你的favorite中,但是List
Favorites实用的类型标记是无界的:getFavorite和putFavorite接受任何Class对象。有时候你可能需要去限制可以传递给方法的类型。这可以通过有界类型令牌来实现,它只是一个类型标记,它对可以表示的类型进行绑定,使用有界类型参数(item30 )或有界通配符(item31)。
注解API(item39 )广泛使用有界类型标记。例如,下面是运行时读取注解的方法。这个方法来自AnnotatedElement接口,该接口由表示类,方法,字段和其他程序元素的反射类型实现:
参数注释类似是一个有界类型标记,表示注释类型。如果该方法有该元素的注解,则返回该元素的注解,如果它没有,则返回null。本质伤,带注解的元素是一个类型安全的异构容器,它的key是注解类型。
假设你有一个类型为Class>的对象,并且希望将它传递给一个需要有界类型令牌的方法,例如getAnnotation。你可以强制将对象转换为Class extends Annotation>,但是这个强制转换是未经检查的,所以它会生成一个编译时警告( item27)。幸运的是,类Class提供了一个实例方法,可以安全地(动态地)执行这种类型的强制转换。这个方法叫作asSubclass,它转换了类对象,在该对象上调用它来表示由其参数表示的类的子类。如果强转成功了,方法返回它的参数;如果失败了,它就抛出ClassCastException。
下面是如何使用asSubclass方法读取在编译时类型未知的注解。这个方法编译时不会有错误或警告:
总之,泛型的正常使用,以集合的API为例,将每个容器的类型参数限制为固定数量。你可以将type参数放置在key上而不是容器上来绕过这个限制。你可以使用Class对象用作此类类型安全的异构容器的key。以这种方法使用的Class对象称为类型令牌。你也可以使用自定义的key类型。例如,你可以使用DatabaseRow类型代表一个数据库列(容器),泛型类型Column
本文写于2019.7.4,历时2天