第三十三条:优先考虑类型安全的异构造器【泛型end】

泛型最常用于集合,如Set<E>和Map<K,V>,以及单个元素的容器,如ThreadLocal<T>和AtomicReference<T>。在所有这些用法中,它都充当被参数化了的容器。这样就限制每个容器只能有固定数目的类型参数。一般来说,这种情况正是你想要的。一个Set只有一个参数类型,表示它的元素类型;一个Map有两个类型参数,表示它的键和值类型。

但是,有时候你会需要更多的灵活性。例如,数据库的行可以有任意数量的列,如果能以类型安全的方式访问所有列就好了。幸运的是,有一种方法可以很容易的做到这一点。这种方法就是将键(key)进行参数化而不是将容器参数化。然后将参数化的键提交给容器来插入或者获取值。用泛型系统来确保值得类型与它的键相符。

下面简单的示范一下这种方法:以Favorites类为例,它允许其客户端从任意数量的其他类中,保存并获取一个“最喜爱“的实例。Class对象充当参数化键的部分。之所以可以这样,是因为Class被泛型化了。类的类型从字面上看起来不再只是简单的Class,而是Class<T>。例如,Stirng.class属于Class<String>类型,Integer.class属于Class<Integer>类型。当一个类的字面被用在方法中,来传达编译时和运行时的类型信息时,就被称作类型令牌(type token)。

// 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。注意,有时Java的printf方法与C语言中的不同,C语言中使用\n的地方,在Java中应该使用%n,这个%n会产生适用特定平台的行分隔符,在许多平台上是\n,但是并非所有平台都是如此。

Favorites实例是类型安全(typesafe)的:当你向它请求String的时候,它从来不会返回一个Integer给你。同样它也是异构的(heterogeneous):不像普通的映射,它的所有键都是不同类型的。因此,我们将Favorites称作类型安全的异构容器(typedafe heterogeneous container)。

Favorites的实现小得出奇。它的完整实现如下:

// 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) {
    return type.cast(favorites.get(type)); 
  }
}

这里发生了一些微妙的事情。每个Favorites实例都得到一个称作favorites的私有Map<Class<?>,Object>的支持。你可能认为由于无限制通配符类型的关系,将不能把任何东西放进这个Map中,但事实正好相反。要注意的是通配符类是嵌套的:它不是属于通配符类型的Map的类型,而是它的键的类型。由此可见,每个键都可以有一个不同的参数化类型:一个可以是Class<String>,接下来是Class<Integer>等。异构就是从这里来的。

第二件要注意的事情是,favorites Map的值类型只是Object。换句话说,Map并不能保证键和值之间的类型关系,即不能保证每个值都为它的键所表示的类型(通俗的说,就是指键与值得类型并不相同)。事实上,Java的类型系统还没有强大到足以表达这一点。但是我们知道这是事实,并在获取faovrite的时候利用了这一点。

putFavorite方法的实现很简单:它只是把(从指定的Class对象到指定的favorite实例)一个映射放到favorite中。如前所述,这是放弃了键和值之间的”类型联系“,因此无法知道这个值是键的一个实例。但是没关系,因为getFavorites方法能够并且的确重新建立了这种联系。

getFavorite方法的实现比putFavorite更难一些。它先从favorites映射中获得与指定Class对象相对应的值。这正是要返回的对象引用,但它的编译时类型是错误的。它的类型只是Object(favorites映射的值类型),我们需要返回一个T。因此,getFavorite方法的实现利用Class的cast方法,将对象引用动态的转换成了对象所表示的类型。

cast方法是Java的转换操作符的动态模拟。它只检验它的参数是否为Class对象所表示的类型的实例。如果是,就返回参数;否则就抛出ClassCastException异常。我们知道getFavorie中的cast永远不会抛出ClassCastException异常,并假设客户端代码正确无误的进行了编译。也就是说,我们知道favorites映射中的值会始终与键的类型相匹配。

假设cast方法只返回它的参数,那它能为我们做什么呢?cast方法的签名充分利用了Class类被泛型化的这个事实。它的返回类型是Class对象的类型参数。

public class Class<T> { 
  T cast(Object obj);
}

这正是getFavorite方法所需要的,也正是让我们不必借助于未受检的转换成T就能确保Favorites类型安全的东西。

Favorites类有两种局限性值得注意。首先,恶意的客户端可以很轻松的破坏Favories实例的类型安全,只要它以原生态形式(raw form)适用Class对象。但是会造成客户端代码在编译时产生未受检的警告。这与一般的集合实现,如HashSet和HashMap并没有什么区别。你可以很容易的利用原生态类型HashSet(详见第26条)将String放进HashSet<Integer>中。也就是说,如果愿意付出一点点代价,就可以拥有运行时的类型安全。确保Favorites永远不违背它的类型约束条件的方式是,让putFavorite方法检验instance是否真的是type所表示的类型的实例。只需使用一个动态的转换,如下代码所示:

// Achieving runtime type safety 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)类型中(详见第28条)。换句话说,你可以保存最喜爱的String或者String[],但是不能保存最喜爱的List<String>。你可以保存最喜爱的String或者String[],但不能保存最喜爱的List<String>。换句话说,如果试图保存最喜爱的List<String>,程序就不能进行编译。原因在于你无法为List<String>获得一个Class对象:List<String>.class是个语法错误,这是件好事。List<String>和List<Integer>共用一个Class对象,即List.class。如果从“类型安全的字面”上来看,List<String>.class和List<Integer>.class是合法的,并返回了相同的对象引用,这就破坏了Favorites对象的内部结构。对于这种局限性,还没有完全令人满意的解决办法。

Favorites使用的类型令牌(type token)是无限制的:getFavorite和putFavtite接受任何Class对象。有时可能需要限制那些可以传给方法的类型。这可以通过有限制的类型令牌来实现,它只要一个类型令牌,利用有限制类型参数(详见第30条)或者有限制通配符来限制可以表示的类型。

注解API(详见第39条)广泛利用了有限制的类型令牌。例如,这是一个在运行时读取注解的方法。这个方法来自AnnotatedElement接口,它通过表示类、方法、域及其它程序元素的反射类型来实现:

public <T extends Annotation> T getAnnotation(Class<T> annotationType);

参数annotionType是一个表示注解类型的有限制的类型令牌。如果元素有这种类型的注解,该方法就将它返回;如果没有,则返回null。被注解的元素本质上是个类型安全的异构容器,容器的键属于注解类型。

假设你有一个类型为Class<?>的对象,并且想将它传给一个需要有限制的类型令牌的方法,例如getAnnotation。你可以将对象转换成Class<? extends Annotation>,但是这种转换是非受检的,因此会产生一条编译时警告(详见第27条)。幸运的是,类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>作为它的键。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353