generics方法上的多个通配符使Java编译器(和我!)非常困惑

我们首先考虑一个简单的场景( 请参阅ideone.com上的完整源代码 ):

import java.util.*; public class TwoListsOfUnknowns { static void doNothing(List<?> list1, List<?> list2) { } public static void main(String[] args) { List<String> list1 = null; List<Integer> list2 = null; doNothing(list1, list2); // compiles fine! } } 

这两个通配符是不相关的,这就是为什么你可以使用List<String>List<Integer>调用doNothing 。 换句话说,这两个? 可以指完全不同的types。 因此,以下不编译,这是预期的( 也在ideone.com上 ):

 import java.util.*; public class TwoListsOfUnknowns2 { static void doSomethingIllegal(List<?> list1, List<?> list2) { list1.addAll(list2); // DOES NOT COMPILE!!! // The method addAll(Collection<? extends capture#1-of ?>) // in the type List<capture#1-of ?> is not applicable for // the arguments (List<capture#2-of ?>) } } 

到目前为止这么好,但在这里,事情开始变得非常混乱( 如在ideone.com上所见 ):

 import java.util.*; public class LOLUnknowns1 { static void probablyIllegal(List<List<?>> lol, List<?> list) { lol.add(list); // this compiles!! how come??? } } 

上面的代码在Eclipse和sun-jdk-1.6.0.17中为我编译,但是应该如何呢? 是不是我们有一个List<List<Integer>> lol和一个List<String> listTwoListsOfUnknowns类似的两个不相关的通配符情况?

事实上,对这个方向稍作修改并不能编译,这是可以预料的( 如在ideone.com上看到的那样 ):

 import java.util.*; public class LOLUnknowns2 { static void rightfullyIllegal( List<List<? extends Number>> lol, List<?> list) { lol.add(list); // DOES NOT COMPILE! As expected!!! // The method add(List<? extends Number>) in the type // List<List<? extends Number>> is not applicable for // the arguments (List<capture#1-of ?>) } } 

所以看起来编译器在做它的工作,但是我们得到了这个( 在ideone.com上看到 ):

 import java.util.*; public class LOLUnknowns3 { static void probablyIllegalAgain( List<List<? extends Number>> lol, List<? extends Number> list) { lol.add(list); // compiles fine!!! how come??? } } 

再次,我们可能有例如List<List<Integer>> lolList<Float> list ,所以这不应该编译,对不对?

事实上,让我们回到更简单的LOLUnknowns1 (两个无限制的通配符),然后尝试看看我们是否实际上probablyIllegal以任何方式调用LOLUnknowns1 。 我们首先尝试“简单”的情况,并为两个通配符select相同的types( 如在ideone.com上所示 ):

 import java.util.*; public class LOLUnknowns1a { static void probablyIllegal(List<List<?>> lol, List<?> list) { lol.add(list); // this compiles!! how come??? } public static void main(String[] args) { List<List<String>> lol = null; List<String> list = null; probablyIllegal(lol, list); // DOES NOT COMPILE!! // The method probablyIllegal(List<List<?>>, List<?>) // in the type LOLUnknowns1a is not applicable for the // arguments (List<List<String>>, List<String>) } } 

这没有意义! 在这里,我们甚至没有尝试使用两种不同的types,它不能编译! 使它成为List<List<Integer>> lolList<String> list也给出了类似的编译错误! 事实上,从我的实验中,代码编译的唯一方法是如果第一个参数是显式的nulltypes( 如在ideone.com上所见 ):

 import java.util.*; public class LOLUnknowns1b { static void probablyIllegal(List<List<?>> lol, List<?> list) { lol.add(list); // this compiles!! how come??? } public static void main(String[] args) { List<String> list = null; probablyIllegal(null, list); // compiles fine! // throws NullPointerException at run-time } } 

所以问题是,关于LOLUnknowns1LOLUnknowns1aLOLUnknowns1b

  • 什么types的论据probablyIllegal接受?
  • 应该lol.add(list); 编译呢? 是types安全吗?
  • 这是一个编译器错误还是我误解通配符捕获转换规则?

附录A:双重LOL?

如果有人好奇,这个编译好( 如在ideone.com上看到的 ):

 import java.util.*; public class DoubleLOL { static void omg2xLOL(List<List<?>> lol1, List<List<?>> lol2) { // compiles just fine!!! lol1.addAll(lol2); lol2.addAll(lol1); } } 

附录B:嵌套通配符 – 它们究竟意味着什么?

进一步的调查表明,也许多个通配符与这个问题无关,而是一个嵌套的通配符是混淆的来源。

 import java.util.*; public class IntoTheWild { public static void main(String[] args) { List<?> list = new ArrayList<String>(); // compiles fine! List<List<?>> lol = new ArrayList<List<String>>(); // DOES NOT COMPILE!!! // Type mismatch: cannot convert from // ArrayList<List<String>> to List<List<?>> } } 

所以它看起来也许是一个List<List<String>>不是一个List<List<?>> 。 实际上,虽然任何List<E>List<?> ,它看起来不像任何List<List<E>>List<List<?>> ( 如在ideone.com上所见 ):

 import java.util.*; public class IntoTheWild2 { static <E> List<?> makeItWild(List<E> list) { return list; // compiles fine! } static <E> List<List<?>> makeItWildLOL(List<List<E>> lol) { return lol; // DOES NOT COMPILE!!! // Type mismatch: cannot convert from // List<List<E>> to List<List<?>> } } 

出现了一个新的问题,那就是什么是List<List<?>>

如附录B所示,这与多个通配符无关,而是误解了List<List<?>>真正含义。

我们先来提醒自己,Javagenerics是不变的:

  1. 一个Integer是一个Number
  2. List<Integer> 不是 List<Number>
  3. List<Integer> 一个List<? extends Number> List<? extends Number>

我们现在简单地将相同的参数应用到我们的嵌套列表情况(更多细节见附录)

  1. List<String>是(可捕获的) List<?>
  2. 一个List<List<String>> 不能被一个List<List<?>>捕获
  3. List<List<String>> IS (可捕获) List<? extends List<?>> List<? extends List<?>>

有了这个理解,问题中的所有片段都可以解释。 (错误地)认为像List<List<?>>这样的types可以捕获List<List<String>>List<List<Integer>>等types,这是不对的。

就是说, List<List<?>>

  • 不是一个列表,其元素是一些未知types的列表。
    • …那将是一个List<? extends List<?>> List<? extends List<?>>
  • 相反,它是一个列表,其元素是任何types的列表。

片段

这里有一个片段来说明以上几点:

 List<List<?>> lolAny = new ArrayList<List<?>>(); lolAny.add(new ArrayList<Integer>()); lolAny.add(new ArrayList<String>()); // lolAny = new ArrayList<List<String>>(); // DOES NOT COMPILE!! List<? extends List<?>> lolSome; lolSome = new ArrayList<List<String>>(); lolSome = new ArrayList<List<Integer>>(); 

更多片段

这里还有另一个有界嵌套通配符的例子:

 List<List<? extends Number>> lolAnyNum = new ArrayList<List<? extends Number>>(); lolAnyNum.add(new ArrayList<Integer>()); lolAnyNum.add(new ArrayList<Float>()); // lolAnyNum.add(new ArrayList<String>()); // DOES NOT COMPILE!! // lolAnyNum = new ArrayList<List<Integer>>(); // DOES NOT COMPILE!! List<? extends List<? extends Number>> lolSomeNum; lolSomeNum = new ArrayList<List<Integer>>(); lolSomeNum = new ArrayList<List<Float>>(); // lolSomeNum = new ArrayList<List<String>>(); // DOES NOT COMPILE!! 

回到问题

回到这个问题的片段,下面的行为就像预期的那样( 在ideone.com上看到 ):

 public class LOLUnknowns1d { static void nowDefinitelyIllegal(List<? extends List<?>> lol, List<?> list) { lol.add(list); // DOES NOT COMPILE!!! // The method add(capture#1-of ? extends List<?>) in the // type List<capture#1-of ? extends List<?>> is not // applicable for the arguments (List<capture#3-of ?>) } public static void main(String[] args) { List<Object> list = null; List<List<String>> lolString = null; List<List<Integer>> lolInteger = null; // these casts are valid nowDefinitelyIllegal(lolString, list); nowDefinitelyIllegal(lolInteger, list); } } 

lol.add(list); 是非法的,因为我们可能有一个List<List<String>> lolList<Object> list 。 事实上,如果我们注释掉违规的语句,那么代码就会编译,这正是我们在main第一次调用的东西。

问题中所有probablyIllegal非法方法都不是非法的。 他们都完全合法和types安全。 编译器绝对没有错误。 它正在做它应该做的事情。


参考

  • Angelika Langer的Javagenerics常见问题
    • 哪些超types关系存在于genericstypes的实例之间?
    • 我可以创build一个types为通配符参数化types的对象吗?
  • JLS 5.1.10捕捉转换

相关问题

  • 任何简单的方法来解释为什么我不能做的List<Animal> animals = new ArrayList<Dog>()
  • Java嵌套的通配符通用不会编译

附录:捕捉转换的规则

(这是在答案的第一个修改中提出来的;这是对types不变的论证的一个有价值的补充。)

5.1.10捕获转换

G命名一个具有n个formstypes参数的genericstypes声明A 1 … A n ,其对应的边界U 1 … U n 。 存在从G <T 1 … T n >G <S 1 … S n >的捕获转换,其中对于1 <= i <= n

  1. 如果T i是forms的通配types参数? 然后 …
  2. 如果T i是forms的通配types参数? extends ? extends B ,然后…
  3. 如果T i是forms的通配types参数? super ? super B ,然后…
  4. 否则, S i = T i

捕捉转换不是recursion应用的。

这部分可能会令人困惑,特别是关于捕获转换的非recursion应用(在此称为CC ),但关键是不是全部? 可以CC; 这取决于它出现在哪里 。 规则4中没有recursion申请,但当规则2或3适用时,则相应的B i本身可能是CC的结果。

让我们通过几个简单的例子:

  • List<?>可以CC List<String>
    • 这个? 可以通过规则1进行CC
  • List<? extends Number> List<? extends Number>可以CC List<Integer>
    • 这个? 可以通过规则2 CC
    • 在应用规则2时, B i只是Number
  • List<? extends Number> List<? extends Number> 不能 CC List<String>
    • 这个? 可以通过规则2进行CC,但是由于不兼容的types,会发生编译时错误

现在让我们尝试一些嵌套:

  • List<List<?>> can not CC List<List<String>>
    • 规则4适用,CC不recursion,所以? 不能 CC
  • List<? extends List<?>> List<? extends List<?>>可以CC List<List<String>>
    • 第一? 可以通过规则2 CC
    • 在应用规则2时, B i现在是一个List<?> ,它可以是CC List<String>
    • 两者? 可以CC
  • List<? extends List<? extends Number>> List<? extends List<? extends Number>>可以CC List<List<Integer>>
    • 第一? 可以通过规则2 CC
    • 在应用规则2时, B i现在是一个List<? extends Number> List<? extends Number> ,它可以CC List<Integer>
    • 两者? 可以CC
  • List<? extends List<? extends Number>> List<? extends List<? extends Number>> 不能 CC List<List<Integer>>
    • 第一? 可以通过规则2 CC
    • 在应用规则2时, B i现在是一个List<? extends Number> List<? extends Number> ,它可以是CC,但是当应用于List<Integer>时会产生编译时错误
    • 两者? 可以CC

为了进一步说明为什么有些 CC和其他人不能,请考虑以下规则: 不能直接实例化通配符types。 那就是,下面给出一个编译时错误:

  // WildSnippet1 new HashMap<?,?>(); // DOES NOT COMPILE!!! new HashMap<List<?>, ?>(); // DOES NOT COMPILE!!! new HashMap<?, Set<?>>(); // DOES NOT COMPILE!!! 

不过,下面的编译就好了:

  // WildSnippet2 new HashMap<List<?>,Set<?>>(); // compiles fine! new HashMap<Map<?,?>, Map<?,Map<?,?>>>(); // compiles fine! 

WildSnippet2编译的原因是因为,如上所述,没有一个? 可以CC。 在WildSnippet1WildSnippet1或者V (或者两者)的HashMap<K,V>都可以通过CC来实现,通过new非法直接实例化。

  • 不应该接受仿制药的争论。 在LOLUnknowns1b的情况下, null被接受,就像第一个参数被input为List 。 例如,这个编译:

     List lol = null; List<String> list = null; probablyIllegal(lol, list); 
  • 恕我直言lol.add(list); 甚至不应该编译,但作为lol.add()需要一个List<?>types的参数,并作为List适合在List<?>它的作品。
    让我想到这个理论的一个奇怪的例子是:

     static void probablyIllegalAgain(List<List<? extends Number>> lol, List<? extends Integer> list) { lol.add(list); // compiles fine!!! how come??? } 

    lol.add()需要一个types为List<? extends Number>的参数 List<? extends Number>和列表types为List<? extends Integer> List<? extends Integer> ,它适合。它不会工作,如果不匹配。 同样的事情,双LOL和其他嵌套通配符, 只要第一次捕获匹配第二次,一切都可以 (并不应该是)。

  • 再次,我不确定,但它确实看起来像一个错误。

  • 我很高兴不是唯一一个总是使用lolvariables的人。

资源:
http://www.angelikalanger.com ,关于generics的常见问题

编辑:

  1. 加了关于Double Lol的评论
  2. 和嵌套的通配符。

不是专家,但我想我可以理解它。

让我们把你的例子改变成相当的东西,但是有更多不同的types:

 static void probablyIllegal(List<Class<?>> x, Class<?> y) { x.add(y); // this compiles!! how come??? } 

让我们把List更改为[]来更具启发性:

 static void probablyIllegal(Class<?>[] x, Class<?> y) { x.add(y); // this compiles!! how come??? } 

现在,x 不是 某种types的数组。 它是任何types的数组。 它可以包含一个Class<String> 一个Class<Int> 。 这不能用普通的types参数表示:

 static<T> void probablyIllegal(Class<T>[] x //homogeneous! not the same! 

对于任何 T Class<?>都是Class<T>的超types。 如果我们认为一个types是一组对象 ,那么设置 Class<?>是所有Class<T>的所有Class<T> 集合 。 (包括它自己吗?我不知道…)