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

Multiple wildcards on a generic methods makes Java compiler (and me!) very confused

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

1
2
3
4
5
6
7
8
9
10
11
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!
    }
}

这两个通配符是不相关的,这就是为什么可以用ListList来调用doNothing。换句话说,两个?可以指代完全不同的类型。因此,以下内容不会编译,这是预期的(也在ideone.com上):

1
2
3
4
5
6
7
8
9
10
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上所见):

1
2
3
4
5
6
7
import java.util.*;

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

上面的代码在eclipse和ideone.com中为我编译,但是应该这样吗?难道我们不可能有一个List> lol和一个List list,类似于TwoListsOfUnknowns中两个不相关的通配符的情况吗?

事实上,对该方向的以下细微修改并未编译,这是可以预料的(如ideone.com上所见):

1
2
3
4
5
6
7
8
9
10
11
12
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上看到的):

1
2
3
4
5
6
7
8
9
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> lol和一个List list,所以这不应该编译,对吗?

实际上,让我们回到更简单的EDOCX1(两个无界通配符),看看我们是否可以以任何方式调用probablyIllegal。让我们先尝试"简单"大小写,然后为两个通配符选择相同的类型(如ideone.com上所示):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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> lolList list也会产生类似的编译错误!事实上,根据我的实验,代码编译的唯一方法是,如果第一个参数是显式的null类型(如ideone.com上所示):

1
2
3
4
5
6
7
8
9
10
11
12
13
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的问题是:

  • probablyIllegal接受什么类型的论点?
  • lol.add(list);应该编译吗?是打字机吗?
  • 这是编译器错误还是我误解了通配符的捕获转换规则?

附录A:双重LOL?

如果有人好奇的话,这是很好的汇编(如ideone.com上所见):

1
2
3
4
5
6
7
8
9
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:嵌套通配符——它们的真正含义是什么????

进一步的调查表明,也许多个通配符与此问题无关,但更确切地说,嵌套通配符是混淆的根源。

1
2
3
4
5
6
7
8
9
10
11
12
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>。事实上,虽然任何List都是List,但它不像任何List>List>(在ideone.com上看到的那样):

1
2
3
4
5
6
7
8
9
10
11
12
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>


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

让我们首先提醒一下,Java泛型是不变的:

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

  • List是(可由)List捕获的。
  • List>不是(可由)List>所捕获。
  • List>是(可由)List>捕获的。
  • 有了这种理解,就可以解释问题中的所有片段。混淆产生于(错误地)相信像List>这样的类型可以捕获像List>List>等类型。这不是真的。

    也就是说,一个List>

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

    片断

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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>>();

    更多片段

    下面是另一个使用有界嵌套通配符的示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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上所示):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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> lol和一个List list。事实上,如果我们对有问题的语句进行注释,代码就会编译,这正是我们在main中第一次调用时得到的。

    所有的probablyIllegal方法都是非法的。它们都是完全合法的和类型安全的。编译器中绝对没有bug。它做的正是它应该做的。

    工具书类

    • Angelika Langer的Java泛型常见问题解答
      • 在泛型类型的实例化之间存在哪些超级子类型关系?
      • 我可以创建一个类型是通配符参数化类型的对象吗?
    • JLS 5.1.10捕获转换

    相关问题

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

    附录:捕获转换规则

    (This was brought up in the first revision of the answer; it's a worthy supplement to the type invariant argument.)

    5.1.10 Capture Conversion

    Let G name a generic type declaration with n formal type parameters A1…An with corresponding bounds U1…Un. There exists a capture conversion from G1…Tn> to G1…Sn>, where, for 1 <= i <= n:

  • If Ti is a wildcard type argument of the form ? then …
  • If Ti is a wildcard type argument of the form ? extends Bi, then …
  • If Ti is a wildcard type argument of the form ? super Bi, then …
  • Otherwise, Si = Ti.
  • Capture conversion is not applied recursively.

    这一节可能会令人困惑,特别是关于捕获转换(此处为cc)的非递归应用,但关键是并非所有?都可以cc;这取决于它出现的位置。规则4中没有递归应用程序,但是当规则2或3适用时,相应的bi本身可能是cc的结果。

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

    • List可与List配套
      • ?可以按规则1进行CC
    • List可与List配套
      • ?可按规则2进行CC。
      • 在应用规则2时,biis simply Number
    • List不能抄送List
      • ?可以按规则2进行CC,但由于类型不兼容而发生编译时错误。

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

    • List>不能抄送List>
      • 规则4适用,CC不是递归的,所以?不能CC
    • List>可CC List>
      • 第一个?可以按规则2进行CC
      • 在应用规则2时,biis now a List,which can cc List
      • 两个?CAN CC
    • List>可CC List>
      • 第一个?可以按规则2进行CC
      • 在应用规则2时,biis now a List,which can cc List
      • 两个?CAN CC
    • List>不能抄送List>
      • 第一个?可以按规则2进行CC
      • 在应用规则2时,biis now a List,which can cc,but gives a compile time error when applied to List
      • 两个?CAN CC

    为了进一步说明为什么有些?可以CC,而另一些则不能,请考虑以下规则:不能直接实例化通配符类型。也就是说,下面给出了一个编译时错误:

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

    但是,下面的编译很好:

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

    WildSnippet2编译的原因是,如上所述,?中没有一个可以CC。在WildSnippet1中,KVKV两者都可以CC,这使得通过new直接实例化是非法的。


    • 不应接受带有泛型的论点。在LOLUnknowns1b的情况下,null被接受,就好像第一个参数被键入List一样。例如,这确实编译了:

      1
      2
      3
      List lol = null;
      List<String> list = null;
      probablyIllegal(lol, list);
    • imho lol.add(list);甚至不应该编译,但由于lol.add()需要一个List类型的参数,并且清单适合List类型的参数,所以它可以工作。让我想到这个理论的一个奇怪的例子是:

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

      lol.add()需要一个List类型的参数,list类型为List类型,它适合。如果不匹配,就不起作用。对于double lol和其他嵌套的通配符来说,同样的事情,只要第一个捕获与第二个捕获匹配,一切都正常(而souldn不正常)。

    • 再说一次,我不确定,但它看起来确实像个虫子。

    • 我很高兴不是唯一一个一直使用lol变量的人。

    资源:http://www.angelikalanger.com,关于仿制药的常见问题解答

    编辑:

  • 添加了关于Double Lol的评论
  • 和嵌套通配符。

  • 不是专家,但我想我能理解。

    让我们将您的示例改为等效的,但具有更多区别的类型:

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

    让我们将列表更改为[]以更具启发性:

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

    现在,x不是某种类型的类的数组。它是任何类型的类的数组。它可以包含一个Class和一个Class。这不能用普通类型参数表示:

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

    Class是任何T的超级Class类型。如果我们认为一个类型是一组对象,那么集合Class就是所有集合Class对所有T的联合。(包括它的电梯吗?我不知道……