通配符类型 List<?> 与原始类型 List 和具体类型 List<Object> 都不相同。如果说变量 x 具有 List<?> 类型,这表示存在一些 T 类型,其中 x 是 List<T>类型,x 具有相同的结构,尽管我们不知道其元素的具体类型。这并不表示它可以具有任意内容,而是指我们并不了解内容的类型限制是什么 — 但我们知道存在 某种限制。另一方面,原始类型 List 是异构的,我们不能对其元素有任何类型限制,具体类型 List<Object> 表示我们明确地知道它能包含任何对象(当然,泛型的类型系统没有 “列表内容” 的概念,但可以从 List 之类的集合类型轻松地理解泛型)。
public void rebox(Box<?> box) {
box.put(box.get());
}
Rebox.java:8: put(capture#337 of ?) in Box<capture#337 of ?> cannot be applied
to (java.lang.Object)
box.put(box.get());
^
1 error
这个代码看起来应该可以工作,因为取出值的类型符合放回值的类型,然而,编译器生成(令人困惑的)关于 “capture#337 of ?” 与 Object 不兼容的错误消息。
“capture#337 of ?” 表示什么?当编译器遇到一个在其类型中带有通配符的变量,比如 rebox() 的 box 参数,它认识到必然有一些 T ,对这些 T 而言 box 是 Box<T>。它不知道 T 代表什么类型,但它可以为该类型创建一个占位符来指代 T 的类型。占位符被称为这个特殊通配符的捕获(capture)。这种情况下,编译器将名称 “capture#337 of ?” 以 box 类型分配给通配符。每个变量声明中每出现一个通配符都将获得一个不同的捕获,因此在泛型声明 foo(Pair<?,?> x, Pair<?,?> y) 中,编译器将给每四个通配符的捕获分配一个不同的名称,因为任意未知的类型参数之间没有关系。
捕获助手技巧允许我们在处理通配符时绕开编译器的限制。当 rebox() 调用 reboxHelper() 时,它知道这么做是安全的,因为它自身的 box 参数对一些未知的 T 而言一定是 Box<T>。因为类型参数 V 被引入到方法签名中并且没有绑定到其他任何类型参数,它也可以表示任何未知类型,因此,某些未知 T 的 Box<T> 也可能是某些未知 V 的 Box<V>(这和 lambda 积分中的 α 减法原则相似,允许重命名边界变量)。现在 reboxHelper() 中的表达式 box.get() 不再具有 Object 类型,它具有 V 类型 — 并允许将 V 传递给 Box<V>.put()。
我们本来可以将 rebox() 声明为一个泛型方法,类似 reboxHelper(),但这被认为是一种糟糕的 API 设计样式。此处的主要设计原则是 “如果以后绝不会按名称引用,则不要进行命名”。就泛型方法来说,如果一个类型参数在方法签名中只出现一次,它很有可能是一个通配符而不是一个命名的类型参数。一般来说,带有通配符的 API 比带有泛型方法的 API 更简单,在更复杂的方法声明中类型名称的增多会降低声明的可读性。因为在需要时始终可以通过专有的捕获助手恢复名称,这个方法让您能够保持 API 整洁,同时不会删除有用的信息。