第五章 泛型
术语 | 中文含义 | 举例 | 所在条目 |
Parameterized type | 参数化类型 |
| 26 |
Actual type parameter | 实际类型参数 |
| 26 |
Generic type | 泛型类型 |
| 26 |
Formal type parameter | 形式类型参数 |
| 26 |
Unbounded wildcard type | 无限制通配符类型 |
| 26 |
Raw type | 原始类型 |
| 26 |
Bounded type parameter | 限制类型参数 |
| 29 |
Recursive type bound | 递归类型限制 |
| 30 |
Bounded wildcard type | 限制通配符类型 |
| 31 |
Generic method | 泛型方法 |
| 30 |
Type token | 类型令牌 |
| 33 |
26. 不要使用原始类型
如果使用原始类型,则会丧失泛型的所有安全性和表达上的优势。
允许原始类型是为了兼容性
如果要使用List,那么改为List< Object >,或List<?>,保证了集合不变性
例外1:**必须在类字面值(class literals)中使用原始类型。**即List<String>.class
和 List<?>.class
不是合法的。
例外2:在无限制通配符类型以外的参数化类型上使用 instanceof
运算符是非法的。 使用无限制通配符类型代替原始类型不会以任何方式影响 instanceof
运算符的行为。 在这种情况下,尖括号和问号就显得多余。
首选方法:
// Legitimate use of raw type - instanceof operator
if (o instanceof Set) { // Raw type
Set<?> s = (Set<?>) o; // Wildcard type
...
}
27.消除 unchecked 警告
使用泛型编程时,会看到许多编译器警告:
未经检查的强制转换警告
未经检查的方法调用警告
未经检查的参数化可变长度类型警告
未经检查的转换警告。
如果发现自己在长度超过一行的方法或构造方法上使用 SuppressWarnings
注解,则可以将其移到局部变量声明上。
你可能需要声明一个新的局部变量,但这是值得的
如:
return (T[]) Arrays.copyOf(elements, size, a.getClass());
变成如下代码,这样所产生的方法干净地编译,并最小化未经检查的警告被抑制的范围。
@SuppressWarnings("unchecked") T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass());
每当使用 @SuppressWarnings(“unchecked”) 注解时,请添加注释,说明为什么是安全的
总结:未经检查的警告是重要的,不要忽视他们。
每个未经检查的警告代表在运行时出现 ClassCastException
异常的可能性。
尽可能消除这些警告。
如果无法消除未经检查的警告,并且可以证明引发该警告的代码是安全类型的,则可以在尽可能小的范围内使用 @SuppressWarnings(“unchecked”)
注解来禁止警告;
记得记录你决定在注释中抑制此警告的理由。
28.列表优于数组
数组在两个重要方面与泛型不同。
首先,数组是协变的(covariant)。
意味着如果 Sub 是 Super 的子类型,则数组类型 Sub[]
是数组类型 Super[]
的子类型。
相比之下,泛型是不变的(invariant):对于任何两种不同的类型 Type1
和 Type2
,List<Type1>
既不是 List<Type2>
的子类型也不是父类型
这样的话会造成如下代码能编译通过,但运行出错:
// Fails at runtime!
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in"; // Throws ArrayStoreException
而使用泛型则编译会报错:
// Won't compile!
List<Object> ol = new ArrayList<Long>(); // Incompatible types
ol.add("I don't fit in");
数组和泛型之间的第二个主要区别是数组被具体化了(reified)。
这意味着数组在运行时知道并强制它们的元素类型。
相比之下,泛型是通过擦除来实现的,这意味着它们只在编译时执行类型约束,并在运行时丢弃(或擦除)元素类型信息。
创建一个泛型数组是非法的, 因为它不是类型安全的。
如果这是合法的,编译器生成的强制转换程序在运行时可能会因为 ClassCastException
异常而失败。
这将违反泛型类型系统提供的基本保证,如下代码:
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<E>
而不是数组类型 E[]
。
这样可能会牺牲一些简洁性或性能,但作为交换,你会获得更好的类型安全性和互操作性。
例子:
public class Chooser<T> {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
choiceArray = (T[]) choices.toArray();
}
public Object choose() {
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
会得到警告
Chooser.java:9: warning: [unchecked] unchecked cast
choiceArray = (T[]) choices.toArray();
^
required: T[], found: Object[]
where T is a type-variable:
T extends Object declared in class Chooser
编译器告诉你在运行时不能保证强制转换的安全性,因为程序不会知道 T
代表什么类型——因为元素类型信息在运行时会被泛型删除。
该程序可以正常工作吗? 是的,但编译器不能证明这一点。
你可以向自己证明这一点,但是你最好将证据放在注释中,指出消除警告的原因,并使用注解隐藏警告(27):
改为如下即可,不会有警告,在运行时也不会得到 ClassCastException
异常:
public class Chooser<T> {
private final List<T> choiceList;//数组改为list
public Chooser(Collection<T> choices) {
choiceList = new ArrayList<>(choices);
}
public T choose() {
Random rnd = ThreadLocalRandom.current();
return choiceList.get(rnd.nextInt(choiceList.size()));
}
}
总结:数组和泛型具有非常不同的类型规则。
数组是协变和具体化的; 泛型是不变的,类型擦除的。
因此,数组提供运行时类型的安全性,但不提供编译时类型的安全性,反之亦然。
一般来说,数组和泛型不能很好地混合工作,数组需要非常多的强转
如果发现把它们混合在一起,得到编译时错误或者警告,你的第一个冲动应该是用列表来替换数组。
29.优先考虑泛型
消除泛型数组创建的技术有两种,案例代码如下:
public class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new E[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
... // no changes in isEmpty or ensureCapacity
}
第一种规避了对泛型数组创建的禁用:创建一个 Object
数组并将其转换为泛型数组类型
可以加上声明:
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
第二种是将属性元素的类型从 E[]
更改为 Object[]
,当然会有一个错误;
可以通过将从数组中检索到的元素转换为 E
来将此错误更改为警告:
E result = (E) elements[--size];
可以加上声明:
public E pop() {
if (size == 0)
throw new EmptyStackException();
// push requires elements to be of type E, so cast is correct
@SuppressWarnings("unchecked") E result =
(E) elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
第一个更可读:数组被声明为 E[]
类型,清楚地表明它只包含 E
实例。 它也更简洁:在一个典型的泛型类中,你从代码中的许多点读取数组; 第一种技术只需要一次转换(创建数组的地方);
而第二种技术每次读取数组元素都需要单独转换。 因此,第一种技术是优选的并且在实践中更常用。 但是,它确实会造成堆污染(heap pollution)( 32 ):数组的运行时类型与编译时类型不匹配(除非 E
碰巧是 Object
)。 这使得一些程序员非常不安,他们选择了第二种技术,尽管在这种情况下堆的污染是无害的。
30.优先使用泛型方法
像泛型类型一样,泛型方法比需要客户端对输入参数和返回值进行显式强制转换的方法更安全,更易于使用。
像类型一样,应该确保方法可以不用强制转换,这通常意味着它们是泛型的。
应该泛型化现有的方法,其使用需要强制转换。 这使得新用户的使用更容易,而不会破坏现有的客户端
31.使用有界通配符增加 API 的灵活性
如将public 返回类型 方法名(类型< E > 参数)
改为public 返回类型 方法名(类型<?extends E > 参数)
和将public 返回类型 方法名(类型< E > 参数)
改为public 返回类型 方法名(类型<?super E > 参数)
PECS 代表: producer-extends,consumer-super(Get and Put Principle):换句话说,如果一个参数化类型代表一个 T
生产者,使用 <? extends T>
;如果它代表 T
消费者,则使用 <? super T>
。
所有 Comparable
和 Comparator
都是消费者。
如果类型参数在方法声明中只出现一次,则用通配符替换它;
如果它是一个无界类型参数,用一个无界通配符替换它;
如果它是有界类型参数,则用有界通配符替换它。
如下代码会产生错误信息,因为列表的类型是 List<?>
,不能将除 null
外的任何值放入 List<?>
中
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
修改如下:
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)));
}
32.合理地结合泛型和可变参数
非具体化类型是指其运行时表示的信息少于其编译时表示的信息,并且几乎所有泛型和参数化类型都是不可具体化的。
如果方法声明其可变参数为不可具体化类型,编译器将在声明上生成警告。
泛型和可变参数混用可能违反类型安全原则:
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
}
如果可变参数数组仅用于从调用者向方法传递可变数量的参数——毕竟这是可变参数的目的——而没有在数组中存储任何东西(它会覆盖参数)并且不允许对数组的引用进行转义(这会使不受信任的代码能够访问数组);
那么该方法是安全的,可以使用@SafeVarargs进行标注
让另一个方法访问泛型可变参数数组是不安全的,如下代码:
// UNSAFE - Exposes a reference to its generic parameter array!
static <T> T[] toArray(T... args) {
return args;
}
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
}
public static void main(String[] args) {
String[] attributes = pickTwo("Good", "Fast", "Cheap");
}
运行时会抛出 ClassCastException;
尽管它不包含可见的强制类型转换。
但是其实编译器在 pickTwo 返回的值上生成了一个隐藏的 String[] 转换,这样它才可以存储在属性中。
转换失败,因为 Object[] 不是 String[] 的子类型。
这个故障非常令人不安,因为它从实际导致堆污染(toArray)的方法中删除了两个级别,而且在实际参数存储在可变参数数组中之后,不会修改该参数数组。
可以修改如下,使用list来代替数组:
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();
}
public static void main(String[] args) {
List<String> attributes = pickTwo("Good", "Fast", "Cheap");
}
只有两个例外是安全的:
将数组传递给另一个使用 @SafeVarargs 正确注释的可变参数方法是安全的
将数组传递给仅计算数组内容的某个函数的非可变方法也是安全的。
何时使用 SafeVarargs 注释的规则很简单:在每个带有泛型或参数化类型的可变参数的方法上使用 @SafeVarargs;
这样它的用户就不会被不必要的和令人困惑的编译器警告所困扰。
这意味着永远不应该编写像dangerous或toArray 这样不安全的可变参数方法。每当编译器警告你控制的方法中的泛型可变参数可能造成堆污染时,请检查该方法是否安全。
一个通用的可变参数方法如何是安全的:
此方法的逻辑没有在可变参数数组中存储任何东西,并且它不会让数组(或者其副本)出现在不可信的代码中。
总结:
可变参数方法和泛型不能很好地交互,因为可变参数工具是构建在数组之上的漏洞抽象,并且数组具有与泛型不同的类型规则。
虽然泛型可变参数不是类型安全的,但它们是合法的。
如果选择使用泛型(或参数化)可变参数编写方法,首先要确保该方法是类型安全的,然后使用 @SafeVarargs 对其进行注释。
33.考虑类型安全的异构容器
大部分容器的泛型参数都是固定的;
然而,有时候你需要更多的灵活性。
例如,一个数据库行可以具有任意多个列,如果能够以类型安全的方式访问所有列就好了。
有一个简单的方法可以达到这种效果:参数化键而不是容器,然后向容器提供参数化键
以插入或检索值
。
泛型类型系统用于确保值
的类型与键
一致。
案例代码:
//每个 Favorites 实例都由一个名为 favorites 的私有 Map<Class<?>, Object> 支持。
//你可能认为由于通配符类型是无界的,所以无法将任何内容放入此映射中,但事实恰恰相反。
//需要注意的是,通配符类型是嵌套的:通配符类型不是 Map 的类型,而是键的类型。这意味着每个键都可以有不同的参数化类型:一个可以是 Class<String>,下一个是 Class<Integer>,等等。
//这就是异构的原理。
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));
}
}
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);
}
可以看到:
Favorites 的实例是类型安全的:当你向它请求一个 String 类型时,它永远不会返回一个 Integer 类型。
它也是异构的:与普通 Map 不同,所有键
都是不同类型的。
因此,我们将 Favorites 称为一个类型安全异构容器。
Favorites 类有两个值得注意的限制:
首先,恶意客户端很容易通过使用原始形式的类对象破坏 Favorites 实例的类型安全。但是生成的客户端代码在编译时将生成一个 unchecked 警告;可以使用动态转换来避免:
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(type, type.cast(instance));
}
第二个限制是它不能用于不可具体化的类型;
也就是说可以存储的 Favorites 实例类型为 String 类型或 String[],但不能存储 List<String>
。
原因是你不能为 List<String>
获取 Class 对象,List<String>.class
是一个语法错误,这也是一件好事。List<String>
和 List<Integer>
共享一个 Class 对象,即 List.class。如果「字面类型」List<String>.class
和 List<Integer>.class
是合法的,并且返回相同的对象引用,那么它将严重破坏 Favorites 对象的内部结构。
对于这个限制,没有完全令人满意的解决方案。
本质上,注解元素是一个类型安全的异构容器,其键是注解类型;
所以通过getAnnotation可获取注解:
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
假设有一个 Class<?>
类型的对象,并且想要将它传递给需要限定类型令牌(如 getAnnotation
)的方法。
可以将对象转换为 Class<? extends Annotation>
,但是这个转换没有被检查,所以它会产生一个编译时警告( 52 )。
所以class 类提供了一个实例方法,可以安全地(动态地)执行这种类型的强制转换。
该方法称为 asSubclass,它强制转换调用它的 Class 对象以表示由其参数表示的类的子类。 如果强制转换成功,则该方法返回其参数; 如果失败,则抛出一个 ClassCastException。
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>
作为它的键
。