泛型方法上的多个通配符使Java编译器(和我!)非常混淆

pol*_*nts 58 java generics compiler-errors wildcard

让我们首先考虑一个简单的场景(请参阅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!
    }
}
Run Code Online (Sandbox Code Playgroud)

这两个通配符是不相关的,这就是为什么你可以doNothing用a List<String>和a 调用List<Integer>.换句话说,两者?可以指代完全不同的类型.因此,以下不编译,这是预期的(也在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 ?>)
    }
}
Run Code Online (Sandbox Code Playgroud)

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

import java.util.*;

public class LOLUnknowns1 {
    static void probablyIllegal(List<List<?>> lol, List<?> list) {
        lol.add(list); // this compiles!! how come???
    }
}
Run Code Online (Sandbox Code Playgroud)

上面的代码在Eclipse和sun-jdk-1.6.0.17ideone.com中为我编译,但它应该吗?难道我们不可能有一个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 ?>)
    }
}
Run Code Online (Sandbox Code Playgroud)

所以看起来编译器正在完成它的工作,但后来我们得到了这个(如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???
    }
}
Run Code Online (Sandbox Code Playgroud)

同样,我们可能有例如a List<List<Integer>> lol和a 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>)
    }
}
Run Code Online (Sandbox Code Playgroud)

这毫无意义!在这里,我们甚至没有尝试使用两种不同的类型,它不编译!使它成为一个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
    }
}
Run Code Online (Sandbox Code Playgroud)

所以,问题是,关于LOLUnknowns1,LOLUnknowns1aLOLUnknowns1b:

  • 什么类型的参数可以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);
    }
}
Run Code Online (Sandbox Code Playgroud)

附录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<?>>
    }
}
Run Code Online (Sandbox Code Playgroud)

所以它看起来可能List<List<String>>不是一个List<List<?>>.事实上,虽然任何List<E>都是a 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<?>>
    }
}
Run Code Online (Sandbox Code Playgroud)

然后出现了一个新问题:那是List<List<?>>什么?

pol*_*nts 70

作为附录B表示,这无关与多个通配符,而是误解是什么List<List<?>>真正的意思.

让我们首先提醒自己Java泛型是不变的意思:

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

我们现在只是将相同的参数应用于我们的嵌套列表情况(有关更多详细信息,请参阅附录):

  1. A List<String>是(可以通过)aList<?>
  2. List<List<String>>NOT(由可捕获)一List<List<?>>
  3. List<List<String>> IS(由可捕获)一List<? extends List<?>>

有了这种理解,可以解释问题中的所有片段.所以产生了困惑中(错误地)认为像一个类型List<List<?>>可以捕捉类型,如List<List<String>>,List<List<Integer>>等,这是正确的.

那就是List<List<?>>:

  • 不是一个列表,其元素是某一个未知类型的列表.
    • ......那将是一个 List<? extends List<?>>
  • 相反,它是一个列表,其元素是任何类型的列表.

片段

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

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>>();
Run Code Online (Sandbox Code Playgroud)

更多片段

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

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!!
Run Code Online (Sandbox Code Playgroud)

回到问题

要回到问题中的代码段,以下行为符合预期(如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);
    }
}
Run Code Online (Sandbox Code Playgroud)

lol.add(list);是非法的,因为我们可能有a List<List<String>> lol和a List<Object> list.实际上,如果我们注释掉有问题的语句,代码就会编译,而这正是我们对第一次调用所做的main.

问题中的所有probablyIllegal方法都不违法.它们都完全合法且类型安全.编译器中绝对没有错误.它正在做它应该做的事情.


参考

相关问题


附录:捕获转换规则

(这是在答案的第一个修订版中提出的;它是对类型不变量参数的一个有价值的补充.)

5.1.10捕获转换

G命名一个泛型类型声明,其中n个形式类型参数A 1 ... A n具有相应的边界U 1 ... U n.存在从G <T 1 ... T n >G <S 1 ... S n >的捕获转换,其中,对于1 <= i <= n:

  1. 如果T i是表单的通配符类型参数,?那么......
  2. 如果T i是形式? extends B i的通配符类型参数,则...
  3. 如果T i是形式? super B i的通配符类型参数,则...
  4. 否则,S i = T i.

捕获转换不会递归应用.

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

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

  • List<?> 可以CC List<String>
    • ?第1条规则可以CC
  • List<? extends Number> 可以CC List<Integer>
    • ?通过第2条CAN CC
    • 在应用规则2时,B i是简单的Number
  • List<? extends Number>可以 CCList<String>
    • ?通过第2条,但是编译时错误可能发生CC由于不兼容的类型

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

  • List<List<?>>可以 CCList<List<String>>
    • 第4条适用,并且CC不是递归的,所以?可以 CC
  • List<? extends List<?>> 可以CC List<List<String>>
    • 第一个?可以通过规则2进行CC
    • 在应用规则2时,B i现在是a List<?>,它可以是CCList<String>
    • 两者都?可以CC
  • List<? extends List<? extends Number>> 可以CC List<List<Integer>>
    • 第一个?可以通过规则2进行CC
    • 在应用规则2时,B i现在是a List<? extends Number>,它可以是CCList<Integer>
    • 两者都?可以CC
  • List<? extends List<? extends Number>>可以 CCList<List<Integer>>
    • 第一个?可以通过规则2进行CC
    • 在应用规则2时,B i现在是a List<? extends Number>,它可以是CC,但在应用时会产生编译时错误List<Integer>
    • 两者都?可以CC

为了进一步说明为什么?CC和其他人不能这样做,请考虑以下规则:您不能直接实例化通配符类型.也就是说,以下给出了编译时错误:

    // WildSnippet1
    new HashMap<?,?>();         // DOES NOT COMPILE!!!
    new HashMap<List<?>, ?>();  // DOES NOT COMPILE!!!
    new HashMap<?, Set<?>>();   // DOES NOT COMPILE!!!
Run Code Online (Sandbox Code Playgroud)

但是,以下编译就好了:

    // WildSnippet2
    new HashMap<List<?>,Set<?>>();            // compiles fine!
    new HashMap<Map<?,?>, Map<?,Map<?,?>>>(); // compiles fine!
Run Code Online (Sandbox Code Playgroud)

WildSnippet2编译的原因是因为,如上所述,没有一个?可以CC.在WildSnippet1,罐头CC中的K或者V(或两者)HashMap<K,V>,这使得通过new非法直接实例化.

  • `名单<?扩展名单<?extends Number >> can CC List <List <Integer >>`,下一个要点是`List <?扩展名单<?extends Number >>不能CC List <List <Integer >>`.所以它可以或不可以?第四点对我来说似乎是多余的. (7认同)
  • @Colin:找到_“为什么这样设计?”的答案会很棒。我现在正在寻找那个。 (2认同)