这与捕获转换有关。安迪的回答很好,但它没有解释规范是如何工作的。我在这里的答案很长,因为,这是 JLS 中相当密集的部分,但我认为它没有得到太多解释,如果你一步一步地走过它并不难。
捕获转换是一个过程,其中编译器采用通配符类型并用非通配符类型替换(部分)通配符。
带有通配符的参数化类型的超类型是捕获转换后该类型的超类型:
4.10.2. Subtyping among Class and Interface Types
给定一个泛型类型声明C<F<sub>1</sub>,...,F<sub>n</sub>> (n > 0),参数化类型C<R<sub>1</sub>,...,R<sub>n</sub>> 的直接超类型至少有一个R<sub>i</sub> (1 ≤ i ≤ n) 是通配符类型参数,是参数化类型C<X<sub>1</sub>,...,X<sub>n</sub>> 的直接超类型,是对C<R<sub>1</sub>,...,R<sub>n</sub>> 应用捕获转换的结果。
带通配符的参数化类型的成员(包括方法)的类型是捕获转换后该类型的成员的类型:
4.5.2. Members and Constructors of Parameterized Types
令C 为具有类型参数A<sub>1</sub>,...,A<sub>n</sub> 的泛型类或接口声明,并令C<T<sub>1</sub>,...,T<sub>n</sub>> 为C 的参数化,其中,对于 1 ≤ i ≤ n,T<sub>i</sub> 是一种类型(而不是通配符)。那么:
如果C的参数化中的任何类型参数是通配符,那么:
-
C<T<sub>1</sub>,...,T<sub>n</sub>>中的字段、方法和构造函数的类型是C<T<sub>1</sub>,...,T<sub>n</sub>>的捕获转换中的字段、方法和构造函数的类型。
那么捕获转换是如何工作的呢?
假设我们得到以下类声明(选择以更完整地说明过程的某些部分):
class C<V, W extends List<V>> {
void m(V v, W w) {
}
}
以及下面使用这种类型:
C<Number, ?> c = new C<>();
Double tArg = 1.0;
List<Number> uArg = new ArrayList<>();
c.m(tArg, uArg);
为了确定if the argument types may be assigned to the parameter types,我们如何确定c.m的类型?
好吧,首先,如上所述,c.m的参数类型是C<Number, ?>的捕获转换中m的参数类型:
5.1.10. Capture Conversion
让G 用n 类型参数A<sub>1</sub>,...,A<sub>n</sub> 和对应的边界U<sub>1</sub>,...,U<sub>n</sub> 命名一个泛型类型声明。
对于这个例子:
-
G 是 C。
-
A<sub>1</sub> 是 V,绑定 U<sub>1</sub> 是 Object。
-
A<sub>2</sub> 是 W,绑定 U<sub>2</sub> 是 List<V>。
存在从参数化类型G<T<sub>1</sub>,...,T<sub>n</sub>> 到参数化类型G<S<sub>1</sub>,...,S<sub>n</sub>> 的捕获转换...
对于这个例子,G<T<sub>1</sub>,...,T<sub>n</sub>> 是C<Number, ?>:
-
T<sub>1</sub> 是 Number。
-
T<sub>2</sub> 是 ?。
...,其中,对于 1 ≤ i ≤ n:
如果T<sub>i</sub> 是? 形式的通配符类型参数,则S<sub>i</sub> 是一个新类型变量,其上限为U<sub>i</sub>[A<sub>1</sub>:=S<sub>1</sub>,...,A<sub>n</sub>:=S<sub>n</sub>],下限为null 类型。
-
如果T<sub>i</sub> 是? extends B<sub>i</sub> 形式的通配符类型参数,则S<sub>i</sub> 是一个新类型变量,其上限为glb(B<sub>i</sub>, U<sub>i</sub>[A<sub>1</sub>:=S<sub>1</sub>,...,A<sub>n</sub>:=S<sub>n</sub>]),下限为null 类型。
glb(V<sub>1</sub>,...,V<sub>m</sub>) 定义为V<sub>1</sub> & ... & V<sub>m</sub>。
U<sub>i</sub>[A<sub>1</sub>:=S<sub>1</sub>,...,A<sub>n</sub>:=S<sub>n</sub>] 是A<sub>i</sub>(类型参数)的边界,每个类型参数替换为每个相应的类型参数。 (这就是为什么我用一个类型参数声明 C 的原因,该类型参数的边界引用了另一个类型参数:因为它说明了这部分的作用。)
在我们的示例中,对于T<sub>2</sub>(即?),S<sub>2</sub> 是一个新类型变量,其上限为U<sub>2</sub>(即List<V>)与将Number 替换为V。
S<sub>2</sub> 因此是一个新类型变量,其上限为List<Number>。
为简单起见,我将忽略我们有有界通配符的情况,但有界通配符本质上只是将捕获转换为新类型变量,其边界为BoundOfWildcard & BoundOfTypeParameter。此外,如果通配符有下限 (super),那么新类型变量也有下限。
如果T<sub>i</sub> 不是通配符,那么:
- 否则,
S<sub>i</sub> = T<sub>i</sub>。
所以在我们的示例中,S<sub>1</sub> 就是 T<sub>1</sub>,即 Number。
还有:
捕获转换不会递归应用。
我们稍后会谈到。
我们现在知道:
-
S<sub>1</sub> 是 Number。
-
S<sub>2</sub> 是编译器刚刚创建的某个类型变量 FRESH extends List<Number>。
因此C<Number, ?>的捕获转化为C<Number, FRESH>。
现在我们实际上可以回答这个问题了:Double 和 List<Number> 是否可以分别分配给 Number 和 FRESH extends List<Number>?在前一种情况下,是的。在后一种情况下,没有。
这与我们自己以这种方式声明类型变量时表达式无法编译的原因相同:
static <FRESH extends List<Number>> void n() {
C<Number, FRESH> c = new C<>();
Double tArg = 1.0;
List<Number> uArg = new ArrayList<>();
c.m(tArg, uArg);
}
The supertypes of a type variable are:
因此,List<Number> 可能不分配给FRESH,因为List<Number> 是FRESH 的一个超类型。
以此类推,我们也可以这样声明一个类:
class Fresh extends List<Number> {}
C<Number, Fresh> c = new C<>();
Double tArg = 1.0;
List<Number> uArg = new ArrayList<>();
c.m(tArg, uArg);
这可能更熟悉,并且在这种情况下类型之间的关系如何工作并没有什么不同。
换句话说,在我们原来的例子中:
C<Number, ?> c = new C<>();
Double tArg = 1.0;
List<Number> uArg = new ArrayList<>();
c.m(tArg, uArg);
// ^^^^ this
只是一个更复杂的版本:
Object o = ...;
String s = o; // Error: attempting to assign a supertype to its subtype.
并且(在一天结束时)由于大致相同的原因无法编译。
总结
捕获转换采用通配符并将它们转换为类型变量(暂时)。之后,只是子类型化的常规规则会导致这些错误。
例如,给定问题中的代码:
private void addString(List<? extends String> list, String s) {
list.add(s); // does not compile
list.add(list.get(0)); // doesn't compile either
}
在查看表达式 list.add(s) 时,编译器会看到如下内容:
private <CAP#1 extends String>
void addString(List<? extends String> list, String s) {
((List<CAP#1>) list).add( s );
list.add(list.get(0));
}
产生的错误如下:
error: no suitable method found for add(String)
list.add(s); // does not compile
^
method Collection.add(CAP#1) is not applicable
(argument mismatch; String cannot be converted to CAP#1)
method List.add(CAP#1) is not applicable
(argument mismatch; String cannot be converted to CAP#1)
where CAP#1 is a fresh type-variable:
CAP#1 extends String from capture of ? extends String
也就是说,编译器发现方法add(CAP#1)和String不能转换为类型变量CAP#1。
在查看表达式 list.add(list.get(0)) 时,编译器会看到如下内容:
private <CAP#1 extends String, CAP#2 extends String>
void addString(List<? extends String> list, String s) {
list.add(s);
((List<CAP#2>) list).add( ((List<CAP#1>) list).get(0) );
}
产生的错误如下:
error: no suitable method found for add(CAP#1)
list.add(list.get(0)); // doesn't compile either
^
method Collection.add(CAP#2) is not applicable
(argument mismatch; String cannot be converted to CAP#2)
method List.add(CAP#2) is not applicable
(argument mismatch; String cannot be converted to CAP#2)
where CAP#1,CAP#2 are fresh type-variables:
CAP#1 extends String from capture of ? extends String
CAP#2 extends String from capture of ? extends String
也就是说,编译器发现list.get(0)返回CAP#1,发现方法add(CAP#2)但CAP#1不能转换为CAP#2。
(Source for errors.)
那么为什么List<Class<?>> 和其他类似类型有效?
回想一下:
- 否则,[如果
T<sub>i</sub> 不是通配符类型],S<sub>i</sub> = T<sub>i</sub>。
还有:
捕获转换不会递归应用。
所以如果T<sub>i</sub> 是像Class<?> 这样的参数化类型,那么S<sub>i</sub> 就是Class<?>。此外,由于捕获转换不是递归应用的,因此算法在将T<sub>1</sub>,...,T<sub>n</sub> 转换为S<sub>1</sub>,...,S<sub>n</sub> 后停止。新类型未进行捕获转换,新类型变量的边界未进行捕获转换。
我们还可以通过引起一些有趣的错误来验证这确实是编译器所做的:
Map<?, List<?>> m = new HashMap<>();
List<?> list = new ArrayList<>();
list.add(m);
这会产生以下错误:
error: no suitable method found for add(Map<CAP#1,List<?>>)
list.add(m);
^
[…]
(Source.)
请注意,Map 类型捕获中的类型参数 List<?> 会转换为自身。
还有一个:
Map<?, ? extends List<?>> m = new HashMap<>();
List<?> list = new ArrayList<>();
list.add(m);
这会产生以下错误:
error: no suitable method found for add(Map<CAP#1,CAP#2>)
list.add(m);
^
[…]
where CAP#1,CAP#2,CAP#3 are fresh type-variables:
CAP#1 extends Object from capture of ?
CAP#2 extends List<?> from capture of ? extends List<?>
CAP#3 extends Object from capture of ?
(Source.)
请注意,这一次,? extends List<?> 是经过捕获转换的,而绑定的 List<?> 不是。
终于
上述问题的答案是List<? extends String> 中的通配符被捕获转换为新类型变量,但List<Class<? extends String>> 中的通配符不是。