在一个通用方法中使用多个通配符会让Java编译器(以及我!)非常困惑

66

首先,让我们考虑一个简单的场景 (在 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>> lolList<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
    }
}

关于LOLUnknowns1LOLUnknowns1aLOLUnknowns1b,以下是问题:

  • 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<?>>到底是什么呢?


1
如果有人想要深入了解常见问题解答:http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html 我现在就开始了... - polygenelubricants
好的,我正好在这个常见问题解答上。但即使如此,我仍然无法弄清楚为什么这不是一个错误。 - Colin Hebert
相关:https://dev59.com/e0rSa4cB1Zd3GeqPWW0e - polygenelubricants
我真的不知道我是否做得很好,但我已经尝试解释了为什么会是这样在这里 - Eugene
3个回答

75
如附录B所示,这与多个通配符无关,而是对List<List<?>>的真正含义的误解。
让我们首先想起Java泛型不变意味着什么:
  1. IntegerNumber
  2. List<Integer>不是List<Number>
  3. List<Integer>List<? extends Number>
现在我们只需将同样的论点应用于嵌套列表情况(有关更多详细信息,请参见附录)
  1. List<String>是(可捕获的)List<?>
  2. List<List<String>>不是(可捕获的)List<List<?>>
  3. List<List<String>>是(可捕获的)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>>();

更多代码片段

这里是一个带有嵌套边界通配符的另一个示例:

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方法都是合法的和类型安全的。编译器中绝对没有错误。它正在做它应该做的事情。


参考资料

相关问题


附录:捕获转换规则

(这是在答案的第一版中提出的,它是类型不变论据的一个有价值的补充。)

5.1.10 捕获转换

假设G是一个带有n个形式类型参数A1…An及相应边界U1…Un的泛型类型声明。从G<T1…Tn>G<S1…Sn>存在捕获转换,其中对于1 <= i <= n

  1. 如果Ti是形如?的通配符类型参数,则……
  2. 如果Ti是形如? extends Bi的通配符类型参数,则……
  3. 如果Ti是形如? super Bi的通配符类型参数,则……
  4. 否则,Si = Ti

捕获转换不会递归应用。

这一部分可能会让人感到困惑,特别是关于捕获转换(以下简称CC)的非递归应用,但关键在于并非所有的?都可以进行CC;它取决于它出现的位置。第4条规则中没有递归应用,但当规则2或3适用时,相应的Bi本身可能是CC的结果。
让我们通过一些简单的例子来理解:
  • List<?>可以CC List<String>
    • 根据规则1,?可以进行CC
  • List<? extends Number>可以CC List<Integer>
    • 根据规则2,?可以进行CC
    • 在应用规则2时,Bi只是Number
  • List<? extends Number>无法CC List<String>
    • 根据规则2,?可以进行CC,但由于不兼容的类型而导致编译时错误
现在让我们尝试一些嵌套:
  • List<List<?>> 不能 CC List<List<String>>
    • 规则4适用,CC不是递归的,所以?不能CC
  • List<? extends List<?>> 可以 CC List<List<String>>
    • 第一个?可以通过规则2进行CC
    • 在应用规则2时,Bi现在是一个List<?>,它可以CC List<String>
    • 两个?都可以CC
  • List<? extends List<? extends Number>> 可以 CC List<List<Integer>>
    • 第一个?可以通过规则2进行CC
    • 在应用规则2时,Bi现在是一个List<? extends Number>,它可以CC List<Integer>
    • 两个?都可以CC
  • List<? extends List<? extends Number>> 不能 CC List<List<Integer>>
    • 第一个?可以通过规则2进行CC
    • 在应用规则2时,Bi现在是一个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!!!

然而,以下内容可以编译成功:

    // WildSnippet2
    new HashMap<List<?>,Set<?>>();            // compiles fine!
    new HashMap<Map<?,?>, Map<?,Map<?,?>>>(); // compiles fine!
< p > WildSnippet2 能够编译通过的原因是,如上所述,没有任何一个 ? 可以 CC。在 WildSnippet1 中,HashMap<K,V>KV(或两者都有)可能会 CC,这使得通过 new 直接实例化非法。


2
@Colin:找到“为什么会这样设计”的答案会很棒。我现在正在寻找它。 - polygenelubricants
这是一些代码:http://ideone.com/2Yrem。我还是对第40行感到困惑,为什么它不能编译?所以我写了method3,对于第15行我遇到了编译问题。但是当我写了method4之后,它奏效了,为什么第19行可以工作,而第15行不行? - Colin Hebert
1
@Colin:第19行没有什么特别的,lollist具有完全相同的类型。 List<List<? extends Number>>List<List<? extends Integer>>是两种不同的类型。在第一个中,您可以add一个List<Float>,而在第二个中则不行。在第15行,您不能执行lol = list,但是您可以执行lol.addAll(list)(请参见http://ideone.com/jOF1v)。同样,在第40行,您不能将`List<List<? extends Integer>>强制转换为List<List<? extends Number>>`,因为它们是两种不兼容的类型。 - polygenelubricants
8
“List<? extends List<? extends Number>> 可替代 List<List<Integer>>”,而“List<? extends List<? extends Number>> 无法替代 List<List<Integer>>”。所以它能还是不能呢?我认为第四个要点是多余的。 - Malcolm
List<? extends List<? extends Number>> 可以转换为 List<List<Integer>>”是正确的。第二个语句可能是打错了,应该是类似于“List<? extends List<? extends CharSequence >> 无法转换为 List<List<Integer>>”。 - Pavan Kumar
显示剩余7条评论

2
  • No argument with generics should be accepted. In the case of LOLUnknowns1b the null is accepted as if the first argument was typed as List. For example this does compile :

    List lol = null;
    List<String> list = null;
    probablyIllegal(lol, list);
    
  • IMHO lol.add(list); shouldn't even compile but as lol.add() needs an argument of type List<?> and as list fits in List<?> it works.
    A strange example which make me think of this theory is :

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

    lol.add() needs an argument of type List<? extends Number> and list is typed as List<? extends Integer>, it fits in. It won't work if it doesn't match. Same thing for the double LOL, and other nested wildcards, as long as the first capture matches the second one, everything is okay (and souldn't be).

  • Again, I'm not sure but it does really seem like a bug.

  • I'm glad to not be the only one to use lol variables all the time.

资源 :
http://www.angelikalanger.com,一个关于泛型的常见问题解答。

编辑 :

  1. 添加了有关Double Lol的注释。
  2. 以及嵌套通配符。

0

虽然我不是专家,但我认为我能理解它。

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

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不是某种类别的数组。它是任何类别的数组。它可以包含一个Class和一个Class。这不能用普通的类型参数来表达:
static<T> void probablyIllegal(Class<T>[] x  //homogeneous! not the same!

Class<?>Class<T> 的超类型,适用于任何 T。如果我们将一个 类型 理解为一组对象,那么 集合 Class<?> 就是所有 TClass<T> 集合的并集。(它是否包括自己?我不知道...)


2
List<T> 转换为 T[] 不是一个有效的步骤,因为在 Java 中数组是协变的而泛型是不变的。也就是说,String[] 是一个 Object[],但是 List<String> 不是一个 List<Object> - polygenelubricants
你是正确的。但我只是试图用一些类比来愚弄自己,以便在直觉层面上接受它。这并不意味着我要研究实际的类型理论。 - irreputable

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接