二阶泛型似乎与一阶泛型不同

时间:2017-09-05 08:26:26

标签: java generics

我认为我对仿制药有一个合理的把握。例如,我理解为什么

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规范相关部分或类似部分的指针。

3 个答案:

答案 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(没有,因为Stringfinal;但是,泛型忽略了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的参数类型:

  

5.1.10. Capture Conversion

     

C<Number, ?>使用 n 类型参数G命名通用类型声明,并使用相应的边界A1,...,An

对于这个例子:

  • U1,...,UnG
  • CA1,绑定VU1
  • ObjectA2,绑定WU2
  

从参数化类型List<V>到参数化类型G<T1,...,Tn>存在捕获转化 ...

对于此示例,G<S1,...,Sn>G<T1,...,Tn>

  • C<Number, ?>T1
  • NumberT2
  

...,其中,1≤ i n

     
      
  • 如果?Ti形式的通配符类型参数,则?是一个新的类型变量,其上限为Si且其下限是Ui[A1:=S1,...,An:=Sn]类型。

  •   
  • 如果nullTi形式的通配符类型参数,则? extends Bi是一个新的类型变量,其上限为Si且其下限是glb(Bi, Ui[A1:=S1,...,An:=Sn])类型。

         

    null定义为glb(V1,...,Vm)

  •   

V1 & ... & VmUi[A1:=S1,...,An:=Sn](类型参数)的边界,每个对应的类型参数都替换每个类型参数。 (这就是为什么我用类型参数声明Ai,其绑定引用了另一个类型参数:因为它说明了这部分的作用。)

在我们的示例中,对于CT2),?是一个新的类型变量,其上限为S2U2使用替换List<V> Number

因此,

V是一个新的类型变量,其上限为S2

为简单起见,我将忽略我们有一个有界通配符的情况,但有界通配符本质上只是捕获转换为一个新的类型变量,其边界为List<Number>。此外,如果通配符具有下限(BoundOfWildcard & BoundOfTypeParameter),则新类型变量也具有下限。

如果super 不是通配符,则:

  
      
  • 否则,Ti
  •   

因此,在我们的示例中,Si = Ti仅为S1 T1

那:

  

不会递归地应用捕获转换。

我们以后会来。

我们现在知道:

  • NumberS1
  • 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>,因为FRESHList<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 Stringadd(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#1add(CAP#2)无法转换为CAP#1

Source for errors.

那么为什么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)

您的示例忽略了这一事实(至少我认为是这样),(对于现有示例,转到IntegerNumberList<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));
}