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
也给出了类似的编译错误! 事实上,从我的实验中,代码编译的唯一方法是如果第一个参数是显式的null
types( 如在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
? extends
B 我 ,然后…- 如果T i是forms的通配types参数
? super
? super
B 我 ,然后…- 否则, 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和其他嵌套通配符, 只要第一次捕获匹配第二次,一切都可以 (并不应该是)。 -
再次,我不确定,但它确实看起来像一个错误。
-
我很高兴不是唯一一个总是使用
lol
variables的人。
资源:
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>
集合 。 (包括它自己吗?我不知道…)