我以为我对仿制药有一个合理的把握.例如,我理解为什么
private void addString(List<? extends String> list, String s) {
list.add(s); // does not compile
list.add(list.get(0)); // doesn't compile either
}
Run Code Online (Sandbox Code Playgroud)
不编译.我甚至凭借这些知识获得了一些互联网业力.
但我认为这个论点不应该编译:
private void addClassWildcard(List<Class<? extends String>> list, Class<? extends String> c) {
list.add(c);
list.add(list.get(0));
}
Run Code Online (Sandbox Code Playgroud)
这也不应该:
private void addClass(List<Class<? extends String>> list, Class<String> c) {
list.add(c);
list.add(list.get(0));
}
Run Code Online (Sandbox Code Playgroud)
但两者都编译.为什么?顶部的例子有什么区别?
我很欣赏普通英语的解释,以及指向Java规范或类似部分的相关部分的指针.
And*_*ner 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
}
Run Code Online (Sandbox Code Playgroud)
这里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
}
Run Code Online (Sandbox Code Playgroud)
这里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);
}
Run Code Online (Sandbox Code Playgroud)
第一种情况是不安全的,因为 - 有一个假设的类扩展String(没有,因为String是final;但是,泛型忽略final),a 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.
Run Code Online (Sandbox Code Playgroud)
这与捕获转换有关.Andy的答案很棒,但它没有解释规范是如何工作的.我的回答很长,因为,这是JLS中相当密集的部分,但是我没有看到它解释得太多,如果你一步一步地走过它并不困难.
捕获转换是一个过程,编译器采用带有通配符的类型,并用不是通配符的类型替换(某些)通配符.
带有通配符的参数化类型的超类型是捕获转换后该类型的超类型:
给出一个通用的类型声明(Ñ > 0),参数类型的直接超其中的至少一个(1≤ 我 ≤ Ñ)是一个通配符类型参数,是参数化的类型的直接超类型是的结果将捕获转换应用于.
C<F1,...,Fn>C<R1,...,Rn>RiC<X1,...,Xn>C<R1,...,Rn>
具有通配符的参数化类型的成员(包括方法)的类型是捕获转换后该类型成员的类型:
让
C是通用类或接口声明类型参数,并让是的参数化,其中,对于1≤ 我 ≤ Ñ,是一种类型的(而不是一个通配符).然后:A1,...,AnC<T1,...,Tn>CTi
- [跳过无关紧要]
如果参数化中的任何类型参数
C是通配符,则:
- 字段,方法和构造函数的类型是捕获转换中的字段,方法和构造函数的类型.
C<T1,...,Tn>C<T1,...,Tn>
假设我们给出了以下类声明(选择以更完整地说明该过程的某些部分):
class C<V, W extends List<V>> {
void m(V v, W w) {
}
}
Run Code Online (Sandbox Code Playgroud)
以下使用此类型:
C<Number, ?> c = new C<>();
Double tArg = 1.0;
List<Number> uArg = new ArrayList<>();
c.m(tArg, uArg);
Run Code Online (Sandbox Code Playgroud)
我们如何确定c.m用于确定参数类型是否可以分配给参数类型的类型?
好吧,首先,如上所述,参数类型c.m是m捕获转换中的参数类型C<Number, ?>:
让我们
G命名一个泛型类型声明,其中n类型参数具有相应的边界.A1,...,AnU1,...,Un
对于这个例子:
G是C.A1是V有约束力的.U1ObjectA2是W有约束力的.U2List<V>存在从参数化类型到参数化类型的捕获转换 ......
G<T1,...,Tn>G<S1,...,Sn>
对于这个例子,是:G<T1,...,Tn>C<Number, ?>
T1是Number.T2是?....,其中,1≤ 我 ≤ ñ:
如果是表单的通配符类型参数,那么它是一个新的类型变量,其上限是,其下限是类型.
Ti?SiUi[A1:=S1,...,An:=Sn]null如果是表单的通配符类型参数,那么它是一个新的类型变量,其上限是,其下限是类型.
Ti? extends BiSiglb(Bi, Ui[A1:=S1,...,An:=Sn])null
glb(V1,...,Vm)被定义为.V1 & ... & Vm
Ui[A1:=S1,...,An:=Sn]是(类型参数)的边界,每个对应的类型参数的每个类型参数的替换.(这就是我用类型参数声明其绑定引用另一个类型参数的原因:因为它说明了这部分的作用.)AiC
在我们的例子中,for (是),是一个新的类型变量,其上限是(即),替换为for .T2?S2U2List<V>NumberV
S2因此是一个新的类型变量,其上限是List<Number>.
为简单起见,我将忽略我们有一个有界通配符的情况,但有界通配符本质上只是捕获转换为一个新的类型变量,其边界是BoundOfWildcard & BoundOfTypeParameter.此外,如果通配符具有下限(super),则新类型变量也具有下限.
如果是没有通配符,则:Ti
- 否则,.
Si = Ti
所以在我们的例子中,就是这个.S1T1Number
然后:
捕获转换不会递归应用.
我们稍后会谈到的.
我们现在知道:
S1是Number.S2是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);
}
Run Code Online (Sandbox Code Playgroud)
- 类型变量的直接超类型是其绑定中列出的类型.
因此,List<Number>可能不会被分配到FRESH,因为List<Number>是AA 超的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);
Run Code Online (Sandbox Code Playgroud)
这可能更熟悉,并且就这种情况下类型之间的关系如何工作而言并没有真正完全不同.
换句话说,在我们的原始示例中:
C<Number, ?> c = new C<>();
Double tArg = 1.0;
List<Number> uArg = new ArrayList<>();
c.m(tArg, uArg);
// ^^^^ this
Run Code Online (Sandbox Code Playgroud)
只是一个更复杂的版本:
Object o = ...;
String s = o; // Error: attempting to assign a supertype to its subtype.
Run Code Online (Sandbox Code Playgroud)
并且(在一天结束时)由于大致相同的原因而无法编译.
捕获转换采用通配符并将其转换为类型变量(临时).在那之后,只是导致这些错误的常规子类型规则.
例如,给出问题中的代码:
private void addString(List<? extends String> list, String s) {
list.add(s); // does not compile
list.add(list.get(0)); // doesn't compile either
}
Run Code Online (Sandbox Code Playgroud)
在查看表达式时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));
}
Run Code Online (Sandbox Code Playgroud)
产生的错误如下:
Run Code Online (Sandbox Code Playgroud)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) );
}
Run Code Online (Sandbox Code Playgroud)
产生的错误如下:
Run Code Online (Sandbox Code Playgroud)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<?>>和其他类似的类型有效呢?回想起那个:
- 否则,[if 不是通配符类型]
Ti, .Si = Ti
然后:
捕获转换不会递归应用.
因此,如果是参数化类型,那么就是.此外,由于捕获转换不是递归应用的,因此算法在转换后才会停止.新类型不是捕获转换的,并且新类型变量的边界不是捕获转换的.TiClass<?>SiClass<?>T1,...,TnS1,...,Sn
我们还可以通过导致一些有趣的错误来验证这确实是编译器的作用:
Map<?, List<?>> m = new HashMap<>();
List<?> list = new ArrayList<>();
list.add(m);
Run Code Online (Sandbox Code Playgroud)
这会产生以下错误:
Run Code Online (Sandbox Code Playgroud)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);
Run Code Online (Sandbox Code Playgroud)
这会产生以下错误:
Run Code Online (Sandbox Code Playgroud)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>是捕获转换为新类型变量,但通配符List<Class<? extends String>>不是.