我认为我对仿制药有一个合理的把握。例如,我理解为什么
private void addString(List<? extends String> list, String s) {
list.add(s); // does not compile
list.add(list.get(0)); // doesn't compile either
}
不编译。 I even earned some internet karma with the knowledge
但是,我认为这不应该编译:
private void addClassWildcard(List<Class<? extends String>> list, Class<? extends String> c) {
list.add(c);
list.add(list.get(0));
}
也不应该这样:
private void addClass(List<Class<? extends String>> list, Class<String> c) {
list.add(c);
list.add(list.get(0));
}
但两者都编译。为什么?顶部的例子有什么区别?
我很欣赏普通英语的解释以及指向Java规范相关部分或类似部分的指针。
答案 0 :(得分:15)
第二种情况是安全的,因为Class<String>
的所有实例都是Class<? extends String>
的实例。
向Class<? extends String>
添加List<Class<? extends String>
的实例并不安全 - 您将使用Class<? extends String>
,get(int)
等返回iterator()
的实例 - 所以这是允许的。
从某种意义上说,Class
中的通配符只有在实际遇到该实例时才会被考虑。请考虑以下示例(从String
切换到Number
,因为String
是最终的。)
private void addClass(List<Class<? extends Number>> list, Class<Number> c) {
list.add(c);
list.add(list.get(0));
}
private void tryItSubclass() {
List<Class<Integer>> ints = new ArrayList<>();
addClass(ints, Number.class); // does not compile
}
此处ints
只能包含Class<Integer>
的实例,但Number.class
也是Class<? extends Number>
,?
被捕获为Number
所以这两个类型不兼容。
private void tryItBound() {
List<Class<Number>> ints = new ArrayList<>();
addClass(ints, Number.class); // does not compile
}
此处ints
只能包含Class<Number>
的实例,但Integer.class
也是Class<? extends Number>
,?
被捕获为Integer
所以这两个类型不兼容。
private void tryItWildcard() {
List<Class<? extends Number>> ints = new ArrayList<>();
addClass(ints, Number.class); // does compile
Class<? extends Number> aClass = ints.get(0);
}
第一种情况是不安全的,因为 - 有一个假设的类扩展了String
(没有,因为String
是final
;但是,泛型忽略了final
}),List<? extends String>
可能是List<HypotheticalClass>
。因此,您无法向String
添加List<? extends String>
,因为您希望该列表中的所有内容都是HypotheticalClass
的实例:
List<HypotheticalClass> list = new ArrayList<>();
List<? extends String> list2 = list;
list2.add(""); // Not allowed, but pretend it is.
HypotheticalClass h = list.get(0); // ClassCastException.
答案 1 :(得分:7)
这与捕获转换有关。安迪的答案很棒,但它并没有解释规范是如何运作的。我的回答很长,因为,这是JLS中非常密集的部分,但是我没有看到它解释得太多,如果你一步一步地完成它并不困难。
捕获转换是一个过程,编译器使用带有通配符的类型,并用不是通配符的类型替换(某些)通配符。
带有通配符的参数化类型的超类型是捕获转换后该类型的超类型:
4.10.2. Subtyping among Class and Interface Types
给定泛型类型声明
C<F1,...,Fn>
( n &gt; 0),参数化类型C<R1,...,Rn>
的直接超类型,其中至少有一个Ri
(1≤ i ≤ n )是通配符类型参数,是参数化类型C<X1,...,Xn>
的直接超类型,它是将捕获转换应用于{的结果{1}}。
带有通配符的参数化类型的成员(包括方法)的类型是捕获转换后该类型成员的类型:
4.5.2. Members and Constructors of Parameterized Types
让
C<R1,...,Rn>
成为类型参数为C
的泛型类或接口声明,让A1,...,An
成为C<T1,...,Tn>
的参数化,其中,1≤ i ≤ n ,C
是一种类型(而不是通配符)。然后:
- [跳过无关紧要]
如果
Ti
参数化中的任何类型参数都是通配符,那么:
C
中字段,方法和构造函数的类型是C<T1,...,Tn>
捕获转换中的字段,方法和构造函数的类型。
假设我们获得了以下类声明(选择以更完整地说明该过程的某些部分):
C<T1,...,Tn>
以下使用此类型:
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
的捕获转换中m
的参数类型:
让
C<Number, ?>
使用 n 类型参数G
命名通用类型声明,并使用相应的边界A1,...,An
。
对于这个例子:
U1,...,Un
是G
。C
为A1
,绑定V
为U1
。Object
为A2
,绑定W
为U2
。从参数化类型
List<V>
到参数化类型G<T1,...,Tn>
存在捕获转化 ...
对于此示例,G<S1,...,Sn>
为G<T1,...,Tn>
:
C<Number, ?>
是T1
。Number
是T2
。...,其中,1≤ i ≤ n :
如果
?
是Ti
形式的通配符类型参数,则?
是一个新的类型变量,其上限为Si
且其下限是Ui[A1:=S1,...,An:=Sn]
类型。如果
null
是Ti
形式的通配符类型参数,则? extends Bi
是一个新的类型变量,其上限为Si
且其下限是glb(Bi, Ui[A1:=S1,...,An:=Sn])
类型。
null
定义为glb(V1,...,Vm)
。
V1 & ... & Vm
是Ui[A1:=S1,...,An:=Sn]
(类型参数)的边界,每个对应的类型参数都替换每个类型参数。 (这就是为什么我用类型参数声明Ai
,其绑定引用了另一个类型参数:因为它说明了这部分的作用。)
在我们的示例中,对于C
(T2
),?
是一个新的类型变量,其上限为S2
(U2
)使用替换List<V>
Number
。
V
是一个新的类型变量,其上限为S2
。
为简单起见,我将忽略我们有一个有界通配符的情况,但有界通配符本质上只是捕获转换为一个新的类型变量,其边界为List<Number>
。此外,如果通配符具有下限(BoundOfWildcard & BoundOfTypeParameter
),则新类型变量也具有下限。
如果super
不是通配符,则:
- 否则,
Ti
。
因此,在我们的示例中,Si = Ti
仅为S1
T1
。
那:
不会递归地应用捕获转换。
我们以后会来。
我们现在知道:
Number
是S1
。Number
是编译器刚创建的某种类型变量S2
。因此,FRESH extends List<Number>
的捕获转换为C<Number, ?>
。
现在我们实际上可以回答这个问题:C<Number, FRESH>
和Double
分别可分配给List<Number>
和Number
吗?在前一种情况下,是的。在后一种情况下,没有。
这是出于同样的原因,如果我们自己以这种方式声明了一个类型变量,表达式就不会编译:
FRESH extends List<Number>
The supertypes of a type variable are:
- 类型变量的直接超类型是其绑定中列出的类型。
因此,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);
}
可能会 分配给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
和其他类似的类型有效?回想一下:
- 否则, [如果
List<Class<?>>
不是通配符类型] ,Ti
。
那:
不会递归地应用捕获转换。
因此,如果Si = Ti
是参数化类型,例如Ti
,则Class<?>
只是Si
。此外,由于未以递归方式应用捕获转换,因此在将Class<?>
转换为T1,...,Tn
后,算法才会停止。新类型不是捕获转换的,并且新类型变量的边界不是捕获转换的。
我们还可以通过导致一些有趣的错误来验证这确实是编译器的作用:
S1,...,Sn
这会产生以下错误:
Map<?, List<?>> m = new HashMap<>(); List<?> list = new ArrayList<>(); list.add(m);
(Source.)
请注意,error: no suitable method found for add(Map<CAP#1,List<?>>)
list.add(m);
^
[…]
类型捕获中的类型参数List<?>
会转换为自身。
另一个:
Map
这会产生以下错误:
Map<?, ? extends List<?>> m = new HashMap<>(); List<?> list = new ArrayList<>(); list.add(m);
(Source.)
请注意,这一次,在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 ?
被捕获转换时,绑定的? extends List<?>
不是。
如上所述问题的答案是List<?>
中的通配符被捕获转换为新的类型变量,但List<? extends String>
中的通配符不是。
答案 2 :(得分:3)
您的示例忽略了这一事实(至少我认为是这样),(对于现有示例,转到Integer
和Number
)List<Class<Integer>>
不是List<Class<? extends Number>>
的有效实例
所以,这不会编译:
public static void main(String[] args) {
List<Class<Integer>> intClasses = new LinkedList<>();
addClass(intClasses, Number.class); // compiler error
}
private static void addClass(List<Class<? extends Number>> list, Class<Number> c) {
list.add(c);
list.add(list.get(0));
}