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> list , TwoListsOfUnknowns类似的两个不相关的通配符情况?
事实上,对这个方向稍作修改并不能编译,这是可以预料的( 如在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>> lol和List<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>> lol和List<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 } }
所以问题是,关于LOLUnknowns1 , LOLUnknowns1a和LOLUnknowns1b :
- 什么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是不变的:
- 一个
Integer是一个Number -
List<Integer>不是List<Number> -
List<Integer>是一个List<? extends Number>List<? extends Number>
我们现在简单地将相同的参数应用到我们的嵌套列表情况(更多细节见附录) :
-
List<String>是(可捕获的)List<?> - 一个
List<List<String>>不能被一个List<List<?>>捕获 -
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>> lol和List<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 :
- 如果T i是forms的通配types参数
?然后 …- 如果T i是forms的通配types参数
? extends? extendsB 我 ,然后…- 如果T i是forms的通配types参数
? super? superB 我 ,然后…- 否则, S i = T i 。
捕捉转换不是recursion应用的。
这部分可能会令人困惑,特别是关于捕获转换的非recursion应用(在此称为CC ),但关键是不是全部? 可以CC; 这取决于它出现在哪里 。 规则4中没有recursion申请,但当规则2或3适用时,则相应的B i本身可能是CC的结果。
让我们通过几个简单的例子:
-
List<?>可以CCList<String>- 这个
?可以通过规则1进行CC
- 这个
-
List<? extends Number>List<? extends Number>可以CCList<Integer>- 这个
?可以通过规则2 CC - 在应用规则2时, B i只是
Number
- 这个
-
List<? extends Number>List<? extends Number>不能 CCList<String>- 这个
?可以通过规则2进行CC,但是由于不兼容的types,会发生编译时错误
- 这个
现在让我们尝试一些嵌套:
-
List<List<?>>can not CCList<List<String>>- 规则4适用,CC不recursion,所以
?不能 CC
- 规则4适用,CC不recursion,所以
-
List<? extends List<?>>List<? extends List<?>>可以CCList<List<String>>- 第一
?可以通过规则2 CC - 在应用规则2时, B i现在是一个
List<?>,它可以是CCList<String> - 两者
?可以CC
- 第一
-
List<? extends List<? extends Number>>List<? extends List<? extends Number>>可以CCList<List<Integer>>- 第一
?可以通过规则2 CC - 在应用规则2时, B i现在是一个
List<? extends Number>List<? extends Number>,它可以CCList<Integer> - 两者
?可以CC
- 第一
-
List<? extends List<? extends Number>>List<? extends List<? extends Number>>不能 CCList<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。 在WildSnippet1 , WildSnippet1或者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的常见问题
编辑:
- 加了关于Double Lol的评论
- 和嵌套的通配符。
不是专家,但我想我可以理解它。
让我们把你的例子改变成相当的东西,但是有更多不同的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> 集合 。 (包括它自己吗?我不知道…)