第五章 泛型
26. 不要使用原始类型
泛型和原始类型
声明中具有一个或多个类型参数的类或接口就是泛型类或泛型接口。泛型类和泛型接口统称为泛型。
每个泛型都定义了一个原始类型,它是没有任何相关类型参数的泛型的名称。例如,List<E>
对应的原始类型是 List。原始类型的行为就好像所有泛型信息从类型声明中删除了一样。它们的存在主要是为了与之前的代码兼容。
泛型优势
泛型可以帮助编译器在编译过程中发现潜在的类型转换异常,而原始类型不行。
泛型的子类型规则
原始类型 List 和 参数化类型 List<Object>
都可以保存任何类型的对象 ,但是不能把其它泛型对象 , 例如List<String>
对象赋给List<Object>
引用而可以赋给 List 引用。泛型有子类型规则,List<String>
是原始类型 List 的子类型,而不是参数化类型 List<Object>
的子类型(Item-28)。假如List<String>
可以是List<Object>
的子类型,那么List<String>
的对象赋给List<Object>
,通过List<Object>
插入非String元素,违反了List<String>
只放String的约定
无界通配符
如果你想使用泛型,但不知道或不关心实际的类型参数是什么,那么可以使用无界通配符 ? 代替。例如,泛型集合 Set<E>
的无界通配符类型是 Set<?>
。它是最通用的参数化集合类型,能够容纳任何集合
无界通配符类型 Set<?>
和原始类型 Set 之间的区别在于通配符类型是安全的,而原始类型不是。将任何元素放入具有原始类型的集合中,很容易破坏集合的类型一致性;而无界通配符类型不能放入元素(除了null)
Set<Integer> integerSet = new HashSet<>();
// 无界通配符类型Set<?>可以引用任何Set<E>和Set,但是不能往里面放除了null的元素
Set<?> set1 = integerSet;
// 原始类型Set也可以引用任何Set<E>和Set,但是可以往里面添加元素,会存在类型转换异常
Set set2 = integerSet;
使用泛型而不用原始类型的例外
- 类字面量。该规范不允许使用参数化类型(尽管它允许数组类型和基本类型)。换句话说,
List.class
,String[].class
和int.class
都是合法的,但是List<String>.class
和List<?>.class
不是。 - instanceof 运算符。由于泛型信息在运行时被删除,因此在不是无界通配符类型之外的参数化类型使用 instanceof 操作符是非法的。使用无界通配符类型代替原始类型不会以任何方式影响 instanceof 运算符的行为。在这种情况下,尖括号和问号只是多余的。下面的例子是使用通用类型 instanceof 运算符的首选方法:
// Legitimate use of raw type - instanceof operator
if (o instanceof Set) { // Raw type
Set<?> s = (Set<?>) o; // Wildcard type
...
}
总之,使用原始类型可能会在运行时导致异常,所以不要轻易使用它们。它们仅用于与引入泛型之前的遗留代码进行兼容。快速回顾一下,Set<Object>
是一个参数化类型,表示可以包含任何类型的对象的集合,Set<?>
是一个无界通配符类型,表示只能包含某种未知类型的对象的集合,Set 是一个原始类型,它没有使用泛型。前两个是安全的,后一个不安全
为便于参考,本条目中介绍的术语(以及后面将要介绍的一些术语)总结如下:
Term | Example | Item |
---|---|---|
Parameterized type | List<String> | Item-26 |
Actual type parameter | String | Item-26 |
Generic type | List<E> | Item-26, Item-29 |
Formal type parameter | E | Item-26 |
Unbounded wildcard type | List<?> | Item-26 |
Raw type | List | Item-26 |
Bounded type parameter | <E extends Number> | Item-29 |
Recursive type bound | <T extends Comparable<T>> | Item-30 |
Bounded wildcard type | List<? extends Number> | Item-31 |
Generic method | static <E> List<E> asList(E[] a) | Item-30 |
Type token | String.class | Item-33 |
27. 消除 unchecked 警告
消除所有 unchecked 警告可以确保代码是类型安全的,运行时不会出现 ClassCastException。如果不能消除警告,但是可以证明引发警告的代码是类型安全的,那么可以使用 SuppressWarnings(“unchecked”) 注解来抑制警告。
SuppressWarnings注解
SuppressWarnings 注解可以用于任何声明中,从单个局部变量声明到整个类。请总是在尽可能小的范围上使用 SuppressWarnings 注解。通常用在一个变量声明或一个非常短的方法或构造函数。不要在整个类中使用 SuppressWarnings。这样做可能会掩盖关键警告。
如果你发现自己在一个超过一行的方法或构造函数上使用 SuppressWarnings 注解,那么你可以将其移动到局部变量声明中
将 SuppressWarnings 注释放在 return 语句上是非法的,因为它不是声明。你可能想把注释放在整个方法上,但是不要这样做。相反,应该声明一个局部变量来保存返回值并添加注解
每次使用 SuppressWarnings(“unchecked”) 注解时,要添加一条注释,说明这样做是安全的。这将帮助他人理解代码,更重要的是,它将降低其他人修改代码而产生不安全事件的几率。如果你觉得写这样的注释很难,那就继续思考合适的方式,你最终可能会发现,unchecked 操作毕竟是不安全的。
总之,unchecked 警告很重要。不要忽视他们。每个 unchecked 警告都代表了在运行时发生 ClassCastException 的可能性。尽最大努力消除这些警告。如果不能消除 unchecked 警告,但是可以证明引发该警告的代码是类型安全的,那么可以在尽可能狭窄的范围内使用 @SuppressWarnings(“unchecked”) 注释来抑制警告。在注释中记录你决定抑制警告的理由。
28. list 优于数组
数组与泛型的区别
-
数组是协变的, 如果 Sub 是 Super 的一个子类型,那么数组类型 Sub[] 就是数组类型 Super[] 的一个子类型。相比之下,泛型是不变的:对于任何两个不同类型 Type1 和 Type2,
List<Type1>
既不是List<Type2>
的子类型,也不是List<Type2>
的父类型。 -
数组是具体化的。这意味着数组在运行时知道并强制执行他们的元素类型。如前所述,如果试图将 String 元素放入一个 Long 类型的数组中,就会得到 ArrayStoreException。相比之下,泛型是通过擦除来实现的,这意味着它们只在编译时执行类型约束,并在运行时丢弃(或擦除)元素类型信息。擦除允许泛型与不使用泛型的遗留代码自由交互操作(Item-26),确保在 Java 5 中平稳过渡
泛型数组的创建是非法的
由于这些基本差异,数组和泛型不能很好地混合。例如,创建泛型、参数化类型或类型参数的数组是非法的。因此,这些数组创建表达式都不是合法的:new List<E>[]、new List<String>[]、new E[]
。所有这些都会在编译时导致泛型数组创建错误。
考虑以下代码片段:
// Why generic array creation is illegal - won't compile!
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> intList = List.of(42); // (2)
Object[] objects = stringLists; // (3)
objects[0] = intList; // (4)
String s = stringLists[0].get(0); // (5)
假设创建泛型数组的第 1 行是合法的。第 2 行创建并初始化一个包含单个元素的 List<Integer>
。第 3 行将 List<String>
数组存储到 Object 类型的数组变量中,这是合法的,因为数组是协变的。第 4 行将 List<Integer>
存储到 Object 类型的数组的唯一元素中,这是成功的,因为泛型是由擦除实现的:List<Integer>
实例的运行时类型是 List,List<String>[]
实例的运行时类型是 List[]
,因此这个赋值不会生成 ArrayStoreException。现在我们有麻烦了。我们将一个 List<Integer>
实例存储到一个数组中,该数组声明只保存 List<String>
实例。在第 5 行,我们从这个数组的唯一列表中检索唯一元素。编译器自动将检索到的元素转换为 String 类型,但它是一个 Integer 类型的元素,因此我们在运行时得到一个 ClassCastException。为了防止这种情况发生,第 1 行(创建泛型数组)必须生成编译时错误。
用List替代数组
当你在转换为数组类型时遇到泛型数组创建错误或 unchecked 强制转换警告时,通常最好的解决方案是使用集合类型 List<E>
,而不是数组类型 E[]
。你可能会牺牲一些简洁性和性能,但作为交换,你可以获得更好地类型安全性和互操作性。
总之,数组和泛型有非常不同的类型规则。数组是协变的、具体化的;泛型是不可变的和可被擦除的。因此,数组提供了运行时类型安全性,而不是编译时类型安全性,对于泛型来说相反。一般来说,数组和泛型不能很好的混合,如果你发现将它们混合在一起并得到编译时错误或警告,那么你的第一个反应应该是将数组替换为 list。
29. 优先使用泛型
编写泛型
在将原始类型修改成泛型时, 可能会遇到不能创建泛型数组的问题,有两种解决方法
- 创建 Object 数组并将其强制转换为 E[] 类型(字段的类型是 E[] ,将Object数组强转成 E[] 可以成功,但是方法返回 E[]给客户端,由编译器添加隐式强转就会失败,因为隐式强转是将Object数组转成声明的具体类型数组)。现在,编译器将发出一个警告来代替错误。这种用法是合法的,但(一般而言)不是类型安全的。编译器可能无法证明你的程序是类型安全的,但你可以。你必须说服自己,unchecked 的转换不会损害程序的类型安全性。所涉及的数组(元素)存储在私有字段中,从未返回给客户端或传递给任何其它方法(传到外面就会发生隐式强转)。添加元素时,元素也是 E 类型,因此 unchecked 的转换不会造成任何损害。一旦你证明了 unchecked 的转换是安全的,就将警告限制在尽可能小的范围内(Item-27)。
- 将字段的类型从 E[] 更改为 Object[]。编译器会产生类似的错误和警告,处理方法也和上面类似
方法1优势:可读性更好,因为数组声明为 E[] 类型,这清楚地表明它只包含 E 的实例。它也更简洁,只需要在创建数组的地方做一次强转,其它地方读取数组元素不用强转成 E 类型
方法1劣势:会造成堆污染(Item-32):数组的运行时类型与其编译时类型不匹配(除非 E 恰好是 Object)
为啥要创建泛型数组
Item-28 鼓励优先使用列表而不是数组。但是在泛型中使用列表并不总是可能或可取的。Java 本身不支持列表,因此一些泛型(如ArrayList)必须在数组之上实现。其它泛型(如HashMap)用数组实现来提高性能
总之,泛型比需要在客户端代码中转换的类型更安全、更容易使用。在设计新类型时,请确保客户端可以在不使用类型转换的情况下使用它们。这通常意味着使类型具有通用性。如果你有任何应该是泛型但不是泛型的现有类型,请对它们进行泛化。这将使这些类型的新用户在不破坏现有客户端的情况下更容易使用。
30. 优先使用泛型方法
类可以是泛型的,方法也可以是泛型的
编写泛型方法
编写泛型方法类似于编写泛型。类型参数列表声明类型参数,它位于方法的修饰符与其返回类型之间。例如,类型参数列表为 <E>
,返回类型为 Set<E>
// Generic method
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
至少对于简单的泛型方法,这就是(要注意细节的)全部。该方法编译时不生成任何警告,并且提供了类型安全性和易用性。这里有一个简单的程序来演示。这个程序不包含转换,编译时没有错误或警告:
// Simple program to exercise generic method
public static void main(String[] args) {
Set<String> guys = Set.of("Tom", "Dick", "Harry");
Set<String> stooges = Set.of("Larry", "Moe", "Curly");
Set<String> aflCio = union(guys, stooges);
System.out.println(aflCio);
}
当你运行程序时,它会打印出 [Moe, Tom, Harry, Larry, Curly, Dick]。(输出元素的顺序可能不同)。
union 方法的一个限制是,所有三个集合(输入参数和返回值)的类型必须完全相同。你可以通过使用有界通配符类型(Item-31)使方法更加灵活。
31. 使用有界通配符增加 API 的灵活性
PECS
为了获得满足里氏代换原则,应对表示生产者或消费者入参使用通配符类型。如果输入参数既是生产者优势消费者,使用精确的类型
PECS助记符:PECS 表示生产者应使用 extends,消费者应使用 super。换句话说,如果参数化类型表示 T 生产者,则使用 <? extends T>
;如果它表示一个 T 消费者,则使用 <? super T>
。不要使用有界通配符类型作为返回类型。 它将强制用户在客户端代码中使用通配符类型,而不是为用户提供额外的灵活性
一个例子
接下来让我们将注意力转移到 Item-30 中的 max 方法,以下是原始声明:
public static <T extends Comparable<T>> T max(List<T> list)
下面是使用通配符类型的修正声明:
public static <T extends Comparable<? super T>> T max(List<? extends T> list)
这里使用了两次 PECS。第一次是参数列表,list作为生产者生成 T 的实例,所以我们将类型从 List<T>
更改为 List<? extends T>
(个人认为这里没有必要改,调用一次max方法只能传入一个类型的List,如果是两个参数,第一个参数的类型决定了T,第二个参数的类型如果是List<? extends T>,那么类型参数必须是T或T的子类。但是如果是泛型类声明的T,那这个改动是有意义的)。第二次是是类型参数 T。这是我们第一次看到通配符应用于类型参数。最初,T 被指定为继承 Comparable<T>
,但是 Comparable<T> 消费 T 实例。因此,将参数化类型 Comparable<T>
替换为有界通配符类型 Comparable<? super T>
,Comparable 始终是消费者,所以一般应优先使用 Comparable<? super T>
而不是 Comparable<T>
,比较器也是如此;因此,通常应该优先使用 Comparator<? super T>
而不是 Comparator<T>
。
修订后的 max 声明可能是本书中最复杂的方法声明。增加的复杂性真的能给你带来什么好处吗?是的。下面是一个简单的列表案例,它在原来的声明中不允许使用,但经修改的声明允许:
List<ScheduledFuture<?>> scheduledFutures = ... ;
不能将原始方法声明应用于此列表的原因是 ScheduledFuture 没有实现 Comparable<ScheduledFuture>
。相反,它是 Delayed 的一个子接口,继承了 Comparable<Delayed>
。换句话说,ScheduledFuture 的实例不仅仅可以与其它 ScheduledFuture 实例进行比较,还可以与任何 Delayed 实例比较,但是原始方法只能和 ScheduledFuture 实例比较。更通俗来说,通配符用于支持不直接实现 Comparable(或 Comparator)但继承了实现 Comparable(或 Comparator)的类型的类型。
类型参数和通配符的对偶性
类型参数和通配符之间存在对偶性,对偶性指实现方式不同但效果一样。例如,下面是swap方法的两种可能声明,第一个使用无界类型参数(Item-30),第二个使用无界通配符:
// Two possible declarations for the swap method
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);
这两个声明哪个更好?在公共API中第二个更好,因为它更简单(客户端可以传入原始类型)。传入一个任意列表,该方法交换索引元素,不需要担心类型参数。通常,如果类型参数在方法声明中只出现一次,则用通配符替换它。如果它是一个无界类型参数,用一个无界通配符替换它;如果它是有界类型参数,则用有界通配符替换它。
交换的第二个声明有一个问题。下面的实现无法编译, list 的类型是 List<?>
,你不能在 List<?>
中放入除 null 以外的任何值。
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
幸运的是,有一种方法可以实现,而无需求助于不安全的强制类型转换或原始类型。其思想是编写一个私有助手方法来捕获通配符类型。为了捕获类型,helper 方法必须是泛型方法。它看起来是这样的:
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
// Private helper method for wildcard capture
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
swapHelper 方法指导 list 是一个 List<E>
。因此,它指导它从这个列表中得到的任何值都是 E 类型的,并且将 E 类型的任何值放入这个列表中都是安全的。这个稍微复杂的实现可以正确编译。它允许我们导出基于通配符的声明,同时在内部利用更复杂的泛型方法。swap 方法的客户端不必面对更复杂的 swapHelper 声明,但它们确实从中受益。值得注意的是,helper 方法具有我们认为对于公共方法过于复杂而忽略的签名。
总之,在 API 中使用通配符类型虽然很棘手,但可以使其更加灵活。如果你编写的库将被广泛使用,则必须考虑通配符类型的正确使用。记住基本规则:生产者使用 extends,消费者使用 super(PECS)。还要记住,所有的 comparable 和 comparator 都是消费者
32. 明智地合用泛型和可变参数
抽象泄露
可变参数方法(Item-53)和泛型都是在 Java 5 中添加,因此你可能认为它们能够优雅地交互;可悲的是,它们并不能。可变参数的目的是允许客户端向方法传递可变数量的参数,但这是一个抽象泄漏:当你调用可变参数方法时,将创建一个数组来保存参数;该数组本应是实现细节,却是可见的。因此,当可变参数具有泛型或参数化类型时,会出现令人困惑的编译器警告。
不可具体化
回想一下 Item-28,不可具体化类型是指其运行时表示的信息少于其编译时表示的信息,并且几乎所有泛型和参数化类型都是不可具体化的。如果方法声明其可变参数为不可具体化类型,编译器将在声明生生成警告。如果方法是在其推断类型不可具体化的可变参数上调用的,编译器也会在调用时生成警告。生成的警告就像这样:
warning: [unchecked] Possible heap pollution from parameterized vararg type List<String>
堆污染
当参数化类型的变量引用不属于该类型的对象时,就会发生堆污染。它会导致编译器自动生成的强制类型转换失败,违反泛型系统的基本保证。
例如,考虑这个方法,它来自 Item-26,但做了些修改:
// Mixing generics and varargs can violate type safety!
// 泛型和可变参数混合使用可能违反类型安全原则!
static void dangerous(List<String>... stringLists) {
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intList; // Heap pollution
String s = stringLists[0].get(0); // ClassCastException
}
方法声明使用泛型可变参数是合法的
这个例子提出了一个有趣的问题:为什么方法声明中使用泛型可变参数是合法的,而显式创建泛型数组是非法的?答案是,带有泛型或参数化类型的可变参数的方法在实际开发中非常有用,因此语言设计人员选择忍受这种不一致性。事实上,Java 库导出了几个这样的方法,包括 Arrays.asList(T... a)
、Collections.addAll(Collection<? super T> c, T... elements)
以及 EnumSet.of(E first, E... rest)
。它们与前面显示的危险方法不同,这些库方法是类型安全的。
在 Java 7 之前,使用泛型可变参数的方法的作者对调用点上产生的警告无能为力。使得这些 API 难以使用。用户必须忍受这些警告,或者在每个调用点(Item-27)使用 @SuppressWarnings(“unchecked”) 注释消除这些警告。这种做法乏善可陈,既损害了可读性,也忽略了标记实际问题的警告。
在 Java 7 中添加了 SafeVarargs 注释,以允许使用泛型可变参数的方法的作者自动抑制客户端警告。本质上,SafeVarargs 注释构成了方法作者的一个承诺,即该方法是类型安全的。 作为这个承诺的交换条件,编译器同意不对调用可能不安全的方法的用户发出警告。
使用SafeVarargs的条件
- 方法没有修改数组元素
- 数组的引用没有逃逸(这会使不受信任的代码能够访问数组)
换句话说,如果可变参数数组仅用于将可变数量的参数从调用方传输到方法(毕竟这是可变参数的目的),那么该方法是安全的。
一个反例
// UNSAFE - Exposes a reference to its generic parameter array!
static <T> T[] toArray(T... args) {
return args;
}
这个方法直接返回了泛型可变参数数组引用,违反了上面的条件2,它可以将堆污染传播到调用堆栈上。
考虑下面的泛型方法,该方法接受三个类型为 T 的参数,并返回一个包含随机选择的两个参数的数组:
public static void main(String[] args) {
String[] attributes = pickTwo("Good", "Fast", "Cheap");
}
static <T> T[] pickTwo(T a, T b, T c) {
switch(ThreadLocalRandom.current().nextInt(3)) {
case 0: return toArray(a, b);
case 1: return toArray(a, c);
case 2: return toArray(b, c);
}
throw new AssertionError(); // Can't get here
}
这段代码编译时不会生成任何警告,运行时会抛出 ClassCastException,尽管它不包含可见的强制类型转换。你没有看到的是,编译器在 pickTwo 返回的值上生成了一个隐藏的 String[] 转换。转换失败,因为 Object[] 实际指向的数组是Object类型的不是String,强转失败。
这个示例的目的是让人明白,让另一个方法访问泛型可变参数数组是不安全的,只有两个例外:将数组传递给另一个使用 @SafeVarargs 正确注释的可变参数方法是安全的,将数组传递给仅计算数组内容的某个函数的非可变方法也是安全的。
扩展
如果 main 方法直接调用 toArray 方法,不会出现 ClassCastException,为什么?泛型不是被擦除了吗,args在运行时不是一个Object[] 吗?非也,运行时args的类型是Object[],但是指向的数组是 String 类型的, 所以编译器添加的强转是可以成功的。下面的例子里,toArray1方法在字节码层面和toArray3方法是一样的,在调用toArray1方法前会先生成 String[],将引用传递给toArray1,
public static void main(String[] args) {
String [] arr1 = toArray1("Alice", "Bob", "Cat");
// ClassCastException
String [] arr2 = toArray2("Alice", "Bob", "Cat");
String [] arr3 = toArray3(new String[]{"Alice", "Bob", "Cat"});
// ClassCastException,多层调用泛型可变参数数组会导致编译器没有足够信息创建真实类型的对象数组(存疑,从字节码来看也是创建了String[])
String[] arr4 = toArray1Crust1("Alice", "Bob", "Cat");
String[] arr5 = toArray1Crust2(new String[]{"Alice", "Bob", "Cat"});
String[] arr6 = toArray1Crust3(new String[]{"Alice", "Bob", "Cat"});
// 泛型方法嵌套没问题
String s = toString1("Alice");
}
static <T> T[] toArray1(T... args) {
return args;
}
static <T> T[] toArray2(T a, T b, T c) {
System.out.println(a.getClass());
// 泛型数组实际类型是Object
T[] result = (T[]) new Object[3];
result[0] = a;
result[1] = b;
result[2] = c;
return result;
}
static <T> T[] toArray3(T[] arr){
return arr;
}
static <T> T[] toArray1Crust1(T...args){
return toArray1(args);
}
static <T> T[] toArray1Crust2(T[] args){
return toArray1(args);
}
static String[] toArray1Crust3(String[] args){
return toArray1(args);
}
private static <T> T toString1(T s) {
return toString2(s);
}
public static <T> T toString2(T s){
return s;
}
一个正确例子
下面是一个安全使用泛型可变参数的典型示例。该方法接受任意数量的列表作为参数,并返回一个包含所有输入列表的元素的序列列表。因为该方法是用 @SafeVarargs 注释的,所以它不会在声明或调用点上生成任何警告:
// Safe method with a generic varargs parameter
@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists)
result.addAll(list);
return result;
}
何时使用SafeVarargs
决定何时使用 SafeVarargs 注释的规则很简单:在每个带有泛型或参数化类型的可变参数的方法上使用 @SafeVarargs,这样它的用户就不会被不必要的和令人困惑的编译器警告所困扰。
请注意,SafeVarargs 注释只能出现在不能覆盖的方法上,因为不可能保证所有可能覆盖的方法都是安全的。在 Java 8 中,注释只能出现在静态方法和final实例方法;在 Java 9 中,它在私有实例方法上也是合法的。
使用 SafeVarargs 注释的另一种选择是接受 Item-28 的建议,并用 List 参数替换可变参数(它是一个伪装的数组)。下面是将这种方法应用到我们的 flatten 方法时的效果。注意,只有参数声明发生了更改:
// List as a typesafe alternative to a generic varargs parameter
static <T> List<T> flatten(List<List<? extends T>> lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists)
result.addAll(list);
return result;
}
然后可以将此方法与静态工厂方法 List.of 一起使用,以允许可变数量的参数。注意,这种方法依赖于 List.of 声明是用 @SafeVarargs 注释的:
audience = flatten(List.of(friends, romans, countrymen));
这种方法的优点是编译器可以证明该方法是类型安全的。你不必使用 SafeVarargs 注释来保证它的安全性,也不必担心在确定它的安全性时可能出错。主要的缺点是客户端代码比较冗长,可能会比较慢。
这种技巧也可用于无法编写安全的可变参数方法的情况,如第 147 页中的 toArray 方法。它的列表类似于 List.of 方法,我们甚至不用写;Java 库的作者为我们做了这些工作。pickTwo 方法变成这样:
static <T> List<T> pickTwo(T a, T b, T c) {
switch(rnd.nextInt(3)) {
case 0: return List.of(a, b);
case 1: return List.of(a, c);
case 2: return List.of(b, c);
}
throw new AssertionError();
}
main 方法是这样的:
public static void main(String[] args) {
List<String> attributes = pickTwo("Good", "Fast", "Cheap");
}
生成的代码是类型安全的,因为它只使用泛型,而不使用数组(List在运行时没有强转,数组会强转)。
总之,可变参数方法和泛型不能很好地交互,并且数组具有与泛型不同的类型规则。虽然泛型可变参数不是类型安全的,但它们是合法的。如果选择使用泛型(或参数化)可变参数编写方法,首先要确保该方法是类型安全的,然后使用 @SafeVarargs 对其进行注释。
33. 考虑类型安全的异构容器
参数化容器
如果你需要存储某种类型的集合,例如存储String的Set,那么Set<String>
就足够了,又比如存储 String-Value 键值对,那么Map<String,Integer>
就足够了
参数化容器的键
如果你要存储任意类型的集合。例如,一个数据库行可以有任意多列,我们希望用一个Map保存该行每列元素。那么我们可以使用参数化容器的键
例子
Favorites 类,允许客户端存储和检索任意多种类型对象。Class 类的对象将扮演参数化键的角色。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());
}
Favorites实例是类型安全的:当你向它请求一个 String 类型时,它永远不会返回一个 Integer 类型。
Favorites实例也是异构的:所有键都是不同类型的,普通 Map 的键是固定一个类型,因此,我们将 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));
}
}
通配符类型的键意味着每个键都可以有不同的参数化类型:一个可以是 Class<String>
,下一个是 Class<Integer>
,等等。这就是异构的原理。
favorites 的值类型仅仅是 Object。换句话说,Map 不保证键和值之间的类型关系
putFavorite 的实现很简单:它只是将从给定 Class 对象到给定对象的映射关系放入 favorites 中。如前所述,这将丢弃键和值之间的「类型关联」;将无法确定值是键的实例。但这没关系,因为 getFavorites 方法可以重新建立这个关联。
getFavorite 的实现比 putFavorite 的实现更复杂。首先,它从 favorites 中获取与给定 Class 对象对应的值。这是正确的对象引用返回,但它有错误的编译时类型:它是 Object(favorites 的值类型),我们需要返回一个 T。因此,getFavorite 的实现通过使用 Class 的 cast 方法,将对象引用类型动态转化为所代表的 Class 对象。
cast 方法是 Java 的 cast 运算符的动态模拟。它只是检查它的参数是否是类对象表示的类型的实例。如果是,则返回参数;否则它将抛出 ClassCastException。我们知道 getFavorite 中的强制转换调用不会抛出 ClassCastException。也就是说,我们知道 favorites 中的值总是与其键的类型匹配。
如果 cast 方法只是返回它的参数,那么它会为我们做什么呢?cast 方法的签名充分利用了 Class 类是泛型的这一事实。其返回类型为 Class 对象的类型参数:
public class Class<T> {
T cast(Object obj);
}
这正是 getFavorite 方法所需要的。它使我们能够使 Favorites 类型安全,而不需要对 T 进行 unchecked 的转换。
Favorites 类有两个值得注意的限制。
-
恶意客户端很容易通过使用原始形式的类对象破坏 Favorites 实例的类型安全。通过使用原始类型 HashSet(Item-26),可以轻松地将 String 类型放入
HashSet<Integer>
中。为了获得运行时的类型安全,让 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)); }
-
Favorites 类不能用于不可具体化的类型(Item-28)。换句话说,你可以存储的 Favorites 实例类型为 String 或 String[],但不能存储
List<String>
。原因是你不能为List<String>
获取 Class 对象,List<String>.class
是一个语法错误
Favorites 使用的类型标记是无界的:getFavorite 和 put-Favorite 接受任何 Class 对象。有时你可能需要限制可以传递给方法的类型。这可以通过有界类型标记来实现,它只是一个类型标记,使用有界类型参数(Item-30)或有界通配符(Item-31)对可以表示的类型进行绑定。
annotation API(Item-39)广泛使用了有界类型标记。例如,下面是在运行时读取注释的方法。这个方法来自 AnnotatedElement 接口,它是由表示类、方法、字段和其他程序元素的反射类型实现的:
public <T extends Annotation>
T getAnnotation(Class<T> annotationType);
参数 annotationType 是表示注释类型的有界类型标记。该方法返回该类型的元素注释(如果有的话),或者返回 null(如果没有的话)。本质上,带注释的元素是一个类型安全的异构容器,其键是注释类型。
假设你有一个 Class<?>
类型的对象,并且希望将其传递给一个需要有界类型标记(例如 getAnnotation)的方法。你可以将对象强制转换为 Class<? extends Annotation>
,但是这个强制转换是 unchecked 的,因此它将生成一个编译时警告(Item-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>
作为它的键