《Effective Java》解读第33条:优先考虑类型安全的异构容器
第33条:优先考虑类型安全的异构容器
这是经典且高级的技巧,突破一个容器只能固定一种或几种类型参数”的限制,实现了在同一个容器中安全地存放和取出多种不同类型的对象。
传统容器的局限性
在普通泛型集合中,如 Map<K, V> 或 Set,容器的类型参数在声明时就被固定了。
// 一旦声明,这个Map就只能放String为键,Integer为值。
Map<String, Integer> scores = new HashMap<>();
scores.put("数学", 90); // 正确
// scores.put(new Date(), 100); // 编译错误,键类型不匹配
// scores.put("语文", "优秀"); // 编译错误,值类型不匹配
这种设计保证了类型安全,但缺乏灵活性。有时我们需要一个更“通用”的容器,比如:
- 一个数据库行,可以存放任意类型的字段(String 姓名, Integer 年龄, Date 生日)。
- 一个上下文,可以存放任意类型的配置项或属性。
- 一个服务定位器,可以根据服务类(如 DatabaseService.class)返回对应类型的服务实例。
如果用 Map<Object, Object>,虽然能放任何东西,但会失去类型安全,取出时全靠危险的强制转换。
使用类型安全的异构容器
将容器的类型参数化,转变为将 “键”进行参数化。然后,用这个参数化的键来检索对应的值。
具体实现步骤:
-
键的类型是
Class<T>:我们使用类的字面常量(Class对象)作为键。因为 Class 本身是泛型的,Class<String>和Class<Integer>是不同类型。 -
值的类型就是键的类型参数 T:这样,键和值之间的类型关系就通过泛型系统绑定起来了。
-
容器本身(如Map)的泛型参数是固定的:通常是一个通配符类型,以适应所有不同类型的 Class 键,例如 Map<Class<?>, Object>。
-
在 put 时利用键的类型来约束值的类型。
-
在 get 时利用键的类型进行安全的转换。
示例
public class Favorites {
// 核心数据结构:键是参数化的Class对象,值是Object
private Map<Class<?>, Object> favorites = new HashMap<>();
// put方法:建立Class<T>到T的实例的映射
public <T> void putFavorite(Class<T> type, T instance) {
// 简单校验,确保instance确实是type的实例(对非null值有效)
favorites.put(Objects.requireNonNull(type), type.cast(instance));
}
// get方法:根据Class<T>键,返回T类型的值
public <T> T getFavorite(Class<T> type) {
// 转换是在这里发生的,但它是类型安全的。
// 1. 从Map取出的是Object。
// 2. cast方法会根据type进行安全的运行时类型转换。
// 3. 由于put时保证了类型约束,这个转换一定会成功(除非外部恶意使用raw type)。
return type.cast(favorites.get(type));
}
}
使用示例:
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);
System.out.printf("%s %x %s%n",
f.getFavorite(String.class),
f.getFavorite(Integer.class),
f.getFavorite(Class.class));
}

-
Favorites 实例 faves 同时安全地存放了 String、Integer、Class 三种不同类型的对象。
-
在取出时,我们得到了完全正确的类型,没有任何编译警告,也无需手动强制转换。
-
类型安全由泛型和 Class.cast() 方法在编译期和运行时共同保证。
关键方法与局限性
- Class.cast() 方法
这是实现运行时类型安全的关键。type.cast(obj) 会在运行时检查 obj 是否可以安全地转换为 type 所代表的类。如果失败则抛出 ClassCastException。在 Favorites 中,由于 put 时已经做了约束,get 时的 cast 几乎总是成功。
- 局限性1:原生类型(Raw Type)破坏
如果客户端恶意使用原生类型,可以破坏类型安全。
faves.putFavorite((Class)Integer.class, "不是整数!"); // 编译警告,但能放进去
Integer i = faves.getFavorite(Integer.class); // 运行时ClassCastException!
在 put 方法中,可以使用 type.cast(instance) 对存入的值进行一次即时检查,如示例代码所示。这能将错误在 put 时就暴露出来,而不是等到 get。
- 局限性2:不可具体化的类型(Non-reifiable Types)
由于 Class 字面常量和泛型擦除机制,你不能用 List.class 或 Class<List> 作为键。例如,无法区分 List 和 List,因为它们擦除后都是 Class。
// 无法编译!
faves.putFavorite(List<String>.class, myStringList);
可以使用“超级类型令牌”(Super Type Token)等高级模式来近似实现,例如通过匿名子类 new TypeReference<List>(){} 来捕获泛型信息。Spring框架的 ParameterizedTypeReference 就是这种思想的体现。
总结
-
目的:设计一个能灵活容纳多种不同类型,同时又能保证编译期和运行时类型安全的容器。
-
关键技术:使用参数化的 Class 对象(即 Class)作为键,将类型的动态信息与容器关联起来,利用泛型系统在编译期检查,利用 Class.cast 在运行时保证安全。
-
优势:比 Map<Object, Object> 安全得多,比为每种类型单独维护一个容器更优雅、更集中。
-
注意:要意识到其局限性,特别是对原生类型攻击的脆弱性和对泛型类型参数的支持不足。
掌握这个模式,标志着你对Java泛型的理解进入了一个更深的层次。它展示了泛型不仅仅用于集合,更可以作为一种强大的类型建模工具。
不过说到底,这条仅仅只能算作是技巧而已。
组合了泛型方法(已有)、Class 类型(已有)、Class.cast() 方法(已有)、Map 数据结构(已有)的使用。
更多推荐

所有评论(0)