Effective Java(3rd)-Item33 考虑类型安全的异构容器

  泛型的常见用途是用于集合,比如Set和Map,以及单元素容器,比如ThreadLocal和AtomicReference。在所有这些用途中,容器都是参数化的。这限制了你在每个容器中有固定数量的类型参数。通常这就是你想要的。Set有一个单独的类型参数,代表了它的元素类型;Map有两个,代表了它的key和value类型;诸如此类。
  然而有时候,你需要更多的灵活性。比如,一个数据库行可以拥有任意多的列,如果它有一个类型安全的方法来访问所有列那就太好了。幸运的是,有一个简单的方式来实现这个效果。这个想法是将key参数化,而不是将容器参数化。然后将容器显示参数化的key,以插入或检索value。泛型类型系统用于保证value的类型与其key一致。
  作为这个方法的简单例子,考虑使用Favorites类允许它的客户端来存储和检索任意多类型的favorite实例。Class对象将扮演参数化key的角色。这样可以工作的原因是Class类是泛型。类文字的类型不是简单的Class而是Class.比如,String.class是Class,Integer.class是Class。当类文字在方法之间传递以同时通信编译时和运行时类型信息时,它被称为*类型令牌 *[Bracha04]。
  Favorites类的API是简单的。它看起来只是一个简单map,除了它的key是参数化的而不是map。客户端在设置和获取favorites时显示一个Class对象。这是它的API:


Effective Java(3rd)-Item33 考虑类型安全的异构容器_第1张图片
image.png

  如下是一个简单的程序使用Favorites类,存储,检索和打印一个favorite String,Integer,Class实例:


Effective Java(3rd)-Item33 考虑类型安全的异构容器_第2张图片
image.png
Effective Java(3rd)-Item33 考虑类型安全的异构容器_第3张图片
image.png

  正如你所期望的,这个程序打印了“Java cafebabe Favorites”.请注意,顺便提一下,Java的printf方法和C的不同,你应该使用%n而在C中使用的是\n。%n生成适用于特定平台的行分隔符,该分隔符在许多但不是所有的平台上都是\n。
  一个Favorites实例是类型安全的:当你问它要一个String的时候它永远不会返回一个Integer。它也是异构的:不像一个原始map。所有key都是不同类型的。所以,我们叫Favorites是类型安全的异构容器。
  Favorites的实现是令人惊讶的瘦小,这是它的完整实现:

Effective Java(3rd)-Item33 考虑类型安全的异构容器_第4张图片
image.png

  这里发生了一些微妙的事情。每个Favorites实例都基于一个叫做favorites的私有的Map,Object>.你可能会认为你不能在这个map中放入任何因为无界通配符类型,但是实际上它可以。需要注意的是,通配符类型是嵌套的:不是map的类型是通配符类型,而是key的类型是。这意味着每一个key都可以有不同的参数化类型:其中一个可以是Class,下一个可以是Class,诸如此类。这就是异构的来源。
  接下来要注意的是,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对象的类型参数:


image.png

  这正是getFavorite方法所需要的。这就允许我们在不依赖于T的未检查的强转的情况下使得Favorites类型安全。
  Favorites类有两个值得注意的限制。首先,一个有恶意的客户端会通过使用原始类型的Class对象来容易地破坏favorites实例的类型安全性。但是,生成的客户端代码在编译时会生成未经检查的警告。这与像HashSet和HashMap这样的正常集合实现没有什么不同。你可以很容易地将一个String放入HashSet,通过原始类型HashSet(item26)。也就是说,如果你愿意未运行时类型的安全性付出代价,就可以保证它的安全性。确保Favorite不会违反其类型不变的方法是使用putFavorite方法来检查实例确实是这个类型代表的类型,我们已经知道怎么做了,只要实用动态转换:


Effective Java(3rd)-Item33 考虑类型安全的异构容器_第5张图片
image.png

  java.util.Collections中有一些集合包装器,他们可以玩同样的把戏。它们叫做checkedSet,checkedList,checkedMap诸如此类。它们的静态工厂除了集合(或map)还接受一个Class对象(或两个)。静态工厂是泛型方法,确保Class对象和集合在编译时类型匹配。包装器为它们包装的集合添加具体化。比如,如果有人尝试将Coin放入你的Collection,包装器就会抛出ClassCastException。这些包装器对于跟踪将错误类型元素添加倒集合中的客户端代码非常有用,该应用程序混合了泛型和原始类型。
  Favorites类的第二个限制是它不能用于不可还原的类型(item28)。换句话说,你可以将String或者String[]存储到你的favorite中,但是List不可以。如果你尝试存储List,你的程序将不能编译。 原因是你不能为List得到一个Class对象。类文字List.class是一个语法错误,同时这也是一件好事。List和List共享一个简单Class对象,是List.class。如果“类型文字”List.class和List.class是合法的,并且返回相同的对象引用,那么它将会破坏Favorites对象的内部。对于这一限制,没有完全令人满意的解决办法。

  Favorites实用的类型标记是无界的:getFavorite和putFavorite接受任何Class对象。有时候你可能需要去限制可以传递给方法的类型。这可以通过有界类型令牌来实现,它只是一个类型标记,它对可以表示的类型进行绑定,使用有界类型参数(item30 )或有界通配符(item31)。

  注解API(item39 )广泛使用有界类型标记。例如,下面是运行时读取注解的方法。这个方法来自AnnotatedElement接口,该接口由表示类,方法,字段和其他程序元素的反射类型实现:

image.png

  参数注释类似是一个有界类型标记,表示注释类型。如果该方法有该元素的注解,则返回该元素的注解,如果它没有,则返回null。本质伤,带注解的元素是一个类型安全的异构容器,它的key是注解类型。
  假设你有一个类型为Class的对象,并且希望将它传递给一个需要有界类型令牌的方法,例如getAnnotation。你可以强制将对象转换为Class,但是这个强制转换是未经检查的,所以它会生成一个编译时警告( item27)。幸运的是,类Class提供了一个实例方法,可以安全地(动态地)执行这种类型的强制转换。这个方法叫作asSubclass,它转换了类对象,在该对象上调用它来表示由其参数表示的类的子类。如果强转成功了,方法返回它的参数;如果失败了,它就抛出ClassCastException。

  下面是如何使用asSubclass方法读取在编译时类型未知的注解。这个方法编译时不会有错误或警告:


Effective Java(3rd)-Item33 考虑类型安全的异构容器_第6张图片
image.png

  总之,泛型的正常使用,以集合的API为例,将每个容器的类型参数限制为固定数量。你可以将type参数放置在key上而不是容器上来绕过这个限制。你可以使用Class对象用作此类类型安全的异构容器的key。以这种方法使用的Class对象称为类型令牌。你也可以使用自定义的key类型。例如,你可以使用DatabaseRow类型代表一个数据库列(容器),泛型类型Column作为它的key。

本文写于2019.7.4,历时2天

你可能感兴趣的:(Effective Java(3rd)-Item33 考虑类型安全的异构容器)