ITEM 33: 优先考虑类型安全的异构容器
ITEM 33: CONSIDER TYPESAFE HETEROGENEOUS CONTAINERS
泛型的常见用途包括集合,如 Set<E> 和 Map<K,V>,以及单元素容器,如 ThreadLocal<T> 和 AtomicReference<T>。在所有这些使用中,参数化的都叫容器。这将每个容器的类型参数限制为固定数量。通常这正是你想要的。Set<E> 具有单个类型参数,表示其元素类型; Map<K,V> 有两种类型,表示其键和值类型。
然而,有时您需要更多的灵活性。例如,一个数据库行可以有任意多的列,能够以一种类型安全的方式访问所有列将是很好的。幸运的是,有一个简单的方法可以达到这种效果。其思想是参数化键而不是容器。然后将参数化的键呈现给容器以插入或检索值。泛型类型系统用于确保值的类型与其键一致。
作为这种方法的一个简单示例,考虑一个 Favorites 类,它允许其客户端存储和检索任意多种类型的 Favorite 实例。类型的类对象将扮演参数化键的角色。这样做的原因是 Class<T> 是泛型的。类型不是简单的 Class,而是 Class<T>。例如, String.class 的类型是Class<String>, Integer.Class 的类型是 Class<Integer>。当类对象在方法之间传递以同时通信编译时和运行时类型信息时,它被称为类型令牌[Bracha04]。Favorites 类的 API 很简单。它看起来就像一个简单的映射,只不过键是参数化的,而不是映射。客户端在设置和获取收藏夹时显示一个类对象。以下是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 类,存储、检索和打印一个 favorite 的 String, Integer, 和 Class instance:
// 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 的地方使用 %n。
Favorites 实例是 typesafe 的:当您向它请求字符串时,它永远不会返回整数。它也是异构的:与普通映射不同,所有键都是不同类型的。因此,我们将 Favorites 称为类型安全异构容器。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 实例都有一个私有 Map<Class<?>, Object> ,称为 favorites。您可能认为由于无界通配符类型,您不能在这个映射中放入任何内容,但事实恰恰相反。需要注意的是通配符类型是嵌套的:通配符类型不是映射的类型,而是它的键的类型。这意味着每个键可以有不同的参数化类型:一个可以是Class<String>,另一个可以是 Class<Integer>,依此类推。这就是异质性的来源。
接下来要注意的是,favorites 映射的值类型只是Object。换句话说,映射不保证键和值之间的类型关系,即每个值都是由键表示的类型。事实上,Java的类型系统还不足以表达这一点。但我们知道这是真的,当我们需要找回 favorite 的时候,我们就会利用它。
putFavorite 方法实现很简单:它只是将一个从给定类对象到给定 favorite 实例的映射放入 favorites 中。如前所述,这将丢弃键和值之间的“类型链接”;它丢失了值是键实例的信息。不过没关系,因为 getFavorites 方法可以而且确实重新建立了这个链接。
getFavorite 的实现比 putFavorite 的实现更复杂。首先,它从 favorites 映射获取与给定类对象对应的值。这是正确的对象引用返回,但它有错误的编译时类型:它是Objeect (favoritesmap 的值类型),我们需要返回一个 T 。因此,getFavorite 方法使用类的 cast 方法动态地将对象引用转换为由类对象表示的类型。
cast 方法是对 Java 的转换操作符的动态模拟。它只是检查它的参数是否是类对象所表示类型的实例。如果是,则返回参数;否则它将抛出一个 ClassCastException。我们知道 getFavorite 中的强制转换调用不会抛出 ClassCastException,假设客户端代码编译得很干净。也就是说,我们知道 favorites 映射中的值总是与其键的类型匹配。
既然 cast 方法只返回它的参数,那么它为我们做了什么呢?cast 方法的签名充分利用了 Class 类是泛型的这一事实。其返回类型为类对象的类型参数:
public class Class<T> {
T cast(Object obj);
}
这正是 getFavorite 方法所需要的。我们能够利用这个方法使 Favorites 成为类型安全的,而不必求助于未检查的对 T 的强制转换。
Favorites 类有两个限制值得注意。首先,恶意客户端可以使用原始形式的类对象,从而很容易破坏 Favorites 实例的类型安全性。但是,生成的客户机代码在编译时将生成未检查的警告。这与普通的集合实现(如 HashSet 和 HashMap)没有什么不同。可以使用原始类型 HashSet(item 26)轻松地将字符串放入 HashSet。也就是说,如果您愿意为此付出代价,您可以拥有运行时类型安全性。确保 Favorites 永远不会违反其类型不变式的方法是让 putFavorite 方法检查实例实际上是由类型表示的类型的实例,我们已经知道如何做到这一点。只需使用动态转换:
// 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,等等。除了集合(或映射),它们的静态工厂还接受一个(或两个)类对象。静态工厂是通用方法,确保类对象和集合的编译时类型匹配。包装器将具体化添加到它们包装的集合中。例如,如果有人试图将 Coin 对象放入 Collection<Stamp>,包装器将在运行时抛出 ClassCastException。在混合了泛型和原始类型的应用程序中,这些包装器对于发现向集合添加错误类型元素的客户机代码非常有用。
Favorites 类的第二个限制是它不能用于不可具体化的类型(item 28)。换句话说,您可以存储您最喜欢的 String or String[],但不能存储 List<String>。如果您试图存储List<String>,您的程序将无法编译。原因是您无法获得List的 Class 对象。 List<String>.class 是一个语法错误,这也是一件好事。List<String> 和 List<Integer>共享一个类对象,即 List.Class。如果 List<String>.class 和 List<Integer>.class 是合法的,并且返回相同的对象引用,这将会严重破坏 Favorites 对象的内部结构。对于这种限制,没有完全令人满意的解决方案。
Favorites 使用的类型标记是无界的:getFavorite 和 putFavorite 接受任何 Class 对象。有时可能需要限制可以传递给方法的类型。这可以通过有界类型令牌来实现,它只是一个类型令牌,使用有界类型参数(item 30)或有界通配符(item 31)对可以表示的类型进行了绑定。
annotation API(item 39)广泛使用了有界类型标记。例如,下面是在运行时读取注解的方法。这个方法来自于带注解的元素接口,它由表示类、方法、字段和其他程序元素的反射类型实现:
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
参数 annotationType 是表示注解类型的有界类型令牌。该方法返回该类型元素的注解(如果有),或者返回null(如果没有)。本质上,带注解的元素是一个类型安全异构容器,其键是注解类型。
假设您有一个 Class<?> 对象,您希望将其传递给需要有界类型令牌(如getAnnotation)的方法。您可以将对象转换为 Class<? extends Annotation>,但此转换是未经检查的,因此将生成编译时警告(item 27)。幸运的是,Class 提供了一个实例方法,可以安全地(动态地)执行这种类型的转换。该方法是 asSubclass,它对类对象进行了强制转换,以表示由其参数表示的类的子类。如果转换成功,则方法返回其参数;如果失败,则抛出 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));
}
总之,以 collections api 为例的泛型的常规使用将每个容器的类型参数限制在固定数量。您可以通过在键上而不是容器上放置类型参数来绕过这个限制。您可以使用类对象作为此类类型安全异构容器的键。以这种方式使用的类对象称为类型标记。您还可以使用自定义密钥类型。例如,可以使用 DatabaseRow 类型表示数据库行(容器),并使用 Column<T> 作为键。