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

2022-08-31 14:49:11

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

这两个通配符是不相关的,这就是为什么您可以使用 a 和 a 进行调用的原因。换句话说,两者可以指完全不同的类型。因此,以下内容不会编译,这是可以预料的(也是在 ideone.com):doNothingList<String>List<Integer>?

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,但是应该吗?难道我们不可能有 a 和 a,从中类似的两个不相关的通配符情况?sun-jdk-1.6.0.17List<List<Integer>> lolList<String> listTwoListsOfUnknowns

事实上,以下对该方向的轻微修改不会编译,这是可以预料的(如 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???
    }
}

同样,我们可能有例如a和a,所以这不应该编译,对吧?List<List<Integer>> lolList<Float> list

事实上,让我们回到更简单的(两个无界通配符),并尝试看看我们是否真的可以以任何方式调用。让我们先尝试“简单”的情况,并为两个通配符选择相同的类型(如 ideone.com 所示):LOLUnknowns1probablyIllegal

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>)
    }
}

这是没有道理的!在这里,我们甚至没有尝试使用两种不同的类型,它也不会编译!使它成为一个,也给出了类似的编译错误!事实上,从我的实验来看,代码编译的唯一方法是第一个参数是显式类型(如 ideone.com 所示):List<List<Integer>> lolList<String> listnull

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<?>>
    }
}

所以它看起来也许a不是一个.事实上,虽然 any 是 a ,但它看起来不像 any 是 a (如 ideone.com 所示):List<List<String>>List<List<?>>List<E>List<?>List<List<E>>List<List<?>>

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

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

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

  1. 一个是IntegerNumber
  2. A 不是 aList<Integer>List<Number>
  3. A AList<Integer>List<? extends Number>

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

  1. A 是(可由)a 捕获List<String>List<?>
  2. A 不是(可由)a 捕获List<List<String>>List<List<?>>
  3. A IS(可捕获者)aList<List<String>>List<? extends List<?>>

有了这种理解,问题中的所有片段都可以解释。混淆产生于(错误地)相信类型 like 可以捕获诸如 、 等类型。事实并非如此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>> lolList<Object> listmain

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


引用

相关问题


附录:捕获转换规则

(这是在答案的第一次修订中提出的;这是对类型不变论证的有价值的补充。

5.1.10 捕获转换

G 使用 n 个正式类型参数命名泛型类型声明 A1...具有相应边界的 n U1...U n.存在从 G<T1...Tn>G<S1...Sn>,其中,对于 1 <= i <= n

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

捕获转换不以递归方式应用。

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

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

  • List<?>可以抄送List<String>
    • 规则 1 的可以 CC?
  • List<? extends Number>可以抄送List<Integer>
    • 规则 2 的可以 CC?
    • 在应用规则 2 时,Bi 只是Number
  • List<? extends Number>不能抄送List<String>
    • can CC 按规则 2,但由于类型不兼容而发生编译时错误?

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

  • List<List<?>>不能抄送List<List<String>>
    • 规则 4 适用,并且 CC 不是递归的,因此不能 CC?
  • List<? extends List<?>>可以抄送List<List<String>>
    • 第一个可以CC规则2?
    • 在应用规则 2 时,Bi 现在是 a ,它可以 CCList<?>List<String>
    • 两者都可以 CC?
  • List<? extends List<? extends Number>>可以抄送List<List<Integer>>
    • 第一个可以CC规则2?
    • 在应用规则 2 时,Bi 现在是 a ,它可以 CCList<? extends Number>List<Integer>
    • 两者都可以 CC?
  • List<? extends List<? extends Number>>不能抄送List<List<Integer>>
    • 第一个可以CC规则2?
    • 在应用规则 2 时,Bi 现在是 a ,它可以 CC,但在应用于List<? extends Number>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!

编译的原因是,如上所述,没有一个可以CC。在 中,can CC 的或(或两者兼而有之),这使得通过直接实例化成为非法。WildSnippet2?WildSnippet1KVHashMap<K,V>new


答案 2
  • 不应接受任何具有泛型的参数。在 接受的情况下,就好像第一个参数被键入为 .例如,这确实编译:LOLUnknowns1bnullList

    List lol = null;
    List<String> list = null;
    probablyIllegal(lol, list);
    
  • 恕我直言,甚至不应该编译,但需要一个类型的参数,并且列表适合它。
    一个让我想起这个理论的奇怪例子是:lol.add(list);lol.add()List<?>List<?>

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

    lol.add()需要一个类型的参数,列表被键入为,它适合。如果它不匹配,它将不起作用。对于双LOL和其他嵌套通配符也是如此,只要第一个捕获与第二个捕获匹配,一切都没问题(并且灵魂不是)。List<? extends Number>List<? extends Integer>

  • 同样,我不确定,但它确实看起来像一个错误。

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

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

编辑:

  1. 添加了有关双 Lol 的评论
  2. 和嵌套通配符。

推荐