Effective Java 第三版——33. 优先考虑类型安全的异构容器

Tips
《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必不少人都读过,号称Java四大名著之一,不过第二版2009年出版,到如今已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深入的变化。
在这里第一时间翻译成中文版。供你们学习分享之用。java

Effective Java, Third Edition

33. 优先考虑类型安全的异构容器

泛型的常见用法包括集合,如Set <E>Map <K,V>和单个元素容器,如ThreadLocal <T>AtomicReference <T>。 在全部这些用途中,它都是参数化的容器。 这限制了每一个容器只能有固定数量的类型参数。 一般这正是你想要的。 一个Set有单一的类型参数,表示它的元素类型; 一个Map有两个,表明它的键和值的类型;等等。数据库

然而有时候,你须要更多的灵活性。 例如,数据库一行记录能够具备任意多列,而且可以以类型安全的方式访问它们是很好的。 幸运的是,有一个简单的方法能够达到这个效果。 这个想法是参数化键(key)而不是容器。 而后将参数化的键提交给容器以插入或检索值。 泛型类型系统用于保证值的类型与其键一致。安全

做为这种方法的一个简单示例,请考虑一个Favorites类,它容许其客户端保存和检索任意多种类型的favorite实例。 该类型的Class对象将扮演参数化键的一部分。其缘由是这Class类是泛型的。 类的类型从字面上来讲不是简单的Class,而是Class <T>。 例如,String.class的类型为Class <String>Integer.class的类型为Class <Integer>。 当在方法中传递字面类传递编译时和运行时类型信息时,它被称为类型令牌(type token)[Bracha04]。学习

Favorites类的API很简单。 它看起来就像一个简单Map类,除了该键是参数化的之外。 客户端在设置和获取favorites实例时呈现一个Class对象。 这里是API:ui

// 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类,保存,检索和打印喜欢的StringIntegerClass实例:翻译

// 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语言的不一样之处在于,应该使用%n,而在C中使用\n%n生成适用的特定于平台的行分隔符,该分隔符在不少但不是全部平台上都是\ncode

Favorites实例是类型安全的:当你请求一个字符串时它永远不会返回一个整数。 它也是异构的:与普通Map不一样,全部的键都是不一样的类型。 所以,咱们将Favorites称为类型安全异构容器(typesafe heterogeneous container.)。对象

Favorites的实现很是小巧。 这是完整的代码:blog

// 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>等等。 这就是异构的由来。token

接下来要注意的是,favorites的Map的值类型只是Object。 换句话说,Map不保证键和值之间的类型关系,即每一个值都是由其键表示的类型。 事实上,Java的类型系统并不足以表达这一点。 可是咱们知道这是真的,并在检索一个favorite时利用了这点。

putFavorite实现很简单:只需将给定的Class对象映射到给定的favorites的实例便可。 如上所述,这丢弃了键和值之间的“类型联系(type linkage)”;没法知道这个值是否是键的一个实例。 但不要紧,由于getFavorites方法能够而且确实从新创建这种关联。

getFavorite的实现比putFavorite更复杂。 首先,它从favorites Map中获取与给定Class对象相对应的值。 这是返回的正确对象引用,但它具备错误的编译时类型:它是Object(favorites map的值类型),咱们须要返回类型T。所以,getFavorite实现动态地将对象引用转换为Class对象表示的类型,使用Class的cast方法。

cast方法是Java的cast操做符的动态模拟。它只是检查它的参数是否由Class对象表示的类型的实例。若是是,它返回参数;不然会抛出ClassCastException异常。咱们知道,假设客户端代码可以干净地编译,getFavorite中的强制转换不会抛出ClassCastException异常。 也就是说,favorites map中的值始终与其键的类型相匹配。

那么这个cast方法为咱们作了什么,由于它只是返回它的参数? cast的签名充分利用了Class类是泛型的事实。 它的返回类型是Class对象的类型参数:

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

这正是getFavorite方法所须要的。 这正是确保Favorites类型安全,而不用求助一个未经检查的强制转换的T类型。

Favorites类有两个限制值得注意。 首先,恶意客户能够经过使用原始形式的Class对象,轻松破坏Favorites实例的类型安全。 但生成的客户端代码在编译时会生成未经检查的警告。 这与正常的集合实现(如HashSet和HashMap)没有什么不一样。 经过使用原始类型HashSet(条目 26),能够轻松地将字符串放入HashSet <Integer>中。 也就是说,若是你愿意为此付出一点代价,就能够拥有运行时类型安全性。 确保Favorites永远不违反类型不变的方法是,使putFavorite方法检查该实例是否由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中有一些集合包装类,能够发挥相同的诀窍。 它们被称为checkedSetcheckedListcheckedMap等等。 他们的静态工厂除了一个集合(或Map)以外还有一个Class对象(或两个)。 静态工厂是泛型方法,确保Class对象和集合的编译时类型匹配。 包装类为它们包装的集合添加了具体化。 例如,若是有人试图将Coin放入你的Collection <Stamp>中,则包装类在运行时会抛出ClassCastException。 这些包装类对于追踪在混合了泛型和原始类型的应用程序中添加不正确类型的元素到集合的客户端代码颇有用。

Favorites类的第二个限制是它不能用于不可具体化的(non-reifiable)类型(条目 28)。 换句话说,你能够保存你最喜欢的StringString [],但不能保存List <String>。 若是你尝试保存你最喜欢的List <String>,程序将不能编译。 缘由是没法获取List <String>的Class对象。 List <String> .class是语法错误,也是一件好事。 List <String>List <Integer>共享一个Class对象,即List.class。 若是“字面类型(type literals)”List <String> .classList <Integer> .class合法并返回相同的对象引用,那么它会对Favorites对象的内部形成严重破坏。 对于这种限制,没有彻底使人满意的解决方法。

Favorites使用的类型令牌( type tokens)是无限制的:getFavoriteputFavorite接受任何Class对象。 有时你可能须要限制可传递给方法的类型。 这能够经过一个有限定的类型令牌来实现,该令牌只是一个类型令牌,它使用限定的类型参数(条目 30)或限定的通配符(条目 31)来放置能够表示的类型的边界。

注解API(条目 39)普遍使用限定类型的令牌。 例如,如下是在运行时读取注解的方法。 此方法来自AnnotatedElement接口,该接口由表示类,方法,属性和其余程序元素的反射类型实现:

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

参数annotationType是表示注解类型的限定类型令牌。 该方法返回该类型的元素的注解(若是它有一个);若是没有,则返回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的一般用法(以集合API为例)限制了每一个容器的固定数量的类型参数。 你能够经过将类型参数放在键上而不是容器上来解决此限制。 可使用Class对象做为此类型安全异构容器的键。 以这种方式使用的Class对象称为类型令牌。 也可使用自定义键类型。 例如,能够有一个表示数据库行(容器)的DatabaseRow类型和一个泛型类型Column <T>做为其键。

相关文章
相关标签/搜索