首先,让我们考虑一个简单的场景 (在 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
的原因。换句话说,这两个 ?
可以指代完全不同的类型。因此,以下代码无法编译,这是可以预期的 (也可在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和ideone.com上的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 ?>)
}
}
看起来编译器在执行它的工作,但是我们得到了这个(如图所示):
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
。让我们先尝试“容易”的情况,并选择相同的类型作为两个通配符(如在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>)
}
}
这毫无意义!我们甚至没有尝试使用两种不同的类型,它就无法编译!将其改为List<List<Integer>> lol
和List<String> list
也会导致类似的编译错误!实际上,从我的实验中可以得知,唯一编译该代码的方法是将第一个参数显式声明为null
类型(在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
,以下是问题:
probablyIllegal
接受哪些类型的参数?lol.add(list);
是否应该编译?它是类型安全的吗?- 这是编译器的错误还是我对通配符的捕获转换规则理解有误?
附录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<?>>
到底是什么呢?