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

标签 java generics wildcard compiler-errors

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

这两个通配符是不相关的,这就是为什么您可以调用 doNothingList<String>和一个 List<Integer> .换句话说,两个?可以指完全不同的类型。因此,以下内容无法编译,这是意料之中的( also on 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 ?>)
    }
}

到目前为止一切顺利,但事情开始变得非常困惑( as seen on ideone.com ):
import java.util.*;

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

上面的代码在 Eclipse 和 sun-jdk-1.6.0.17 上为我编译在 ideone.com 中,但应该吗?我们不可能有 List<List<Integer>> lol和一个 List<String> list ,来自 TwoListsOfUnknowns 的类似的两个不相关通配符情况?

事实上,以下朝该方向的轻微修改不会编译,这是意料之中的( as seen on 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 ?>)
    }
}

所以看起来编译器正在做它的工作,但是我们得到了这个( as seen on 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???
    }
}

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

其实让我们回到更简单的LOLUnknowns1 (两个无界通配符)并尝试查看我们是否实际上可以调用 probablyIllegal以任何方式。让我们先尝试“简单”的情况,并为两个通配符选择相同的类型( as seen on 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类型( as seen on 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 , LOLUnknowns1aLOLUnknowns1b :
  • 什么类型的参数probablyIllegal接受?
  • lol.add(list);完全编译?它是类型安全的吗?
  • 这是编译器错误还是我误解了通配符的捕获转换规则?


  • 附录A:双重LOL?

    如果有人好奇,这可以很好地编译( as seen on 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<?>> ( as seen on 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<?>> ?

    最佳答案

    如附录 B 所示,这与多个通配符无关,而是误解了 List<List<?>>真正意思。
    让我们首先提醒自己 Java 泛型是不变的意味着什么:

  • IntegerNumber
  • 一个 List<Integer>不是 List<Number>
  • 一个 List<Integer>List<? extends Number>

  • 我们现在简单地将相同的参数应用于我们的嵌套列表情况(有关更多详细信息,请参见附录):
  • 一个 List<String>是(可被捕获)List<?>
  • 一个 List<List<String>>不是(可被捕获)List<List<?>>
  • 一个 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>>();
    

    更多片段
    这是另一个带有有界嵌套通配符的示例:
    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!!
    

    回到问题
    回到问题中的片段,以下行为符合预期( as seen on 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问题中的方法不违法。它们都是完全合法且类型安全的。编译器绝对没有错误。它正在做它应该做的事情。

    引用
  • Angelika Langer's Java Generics FAQ
  • Which super-subtype relationships exist among instantiations of generic types?
  • Can I create an object whose type is a wildcard parameterized type?

  • JLS 5.1.10 Capture Conversion

  • 相关问题
  • Any simple way to explain why I cannot do List<Animal> animals = new ArrayList<Dog>() ?
  • Java nested wildcard generic won’t compile

  • 附录:捕获转换规则

    (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 G<T1…Tn> to G<S1…Sn>, where, for 1 <= i <= n:

    1. If Ti is a wildcard type argument of the form ? then …
    2. If Ti is a wildcard type argument of the form ? extends Bi, then …
    3. If Ti is a wildcard type argument of the form ? super Bi, then …
    4. Otherwise, Si = Ti.

    Capture conversion is not applied recursively.


    本节可能会令人困惑,特别是关于捕获转换的非递归应用(特此 CC),但关键是并非所有 ?可以抄送;这取决于它出现的位置。规则 4 中没有递归应用,但是当规则 2 或 3 应用时,相应的 Bi 本身可能是 CC 的结果。
    让我们来看看几个简单的例子:
  • List<?>可以抄送List<String>
  • ?可以按规则 1 抄送

  • List<? extends Number>可以抄送List<Integer>
  • ?可以按规则 2 抄送
  • 在应用规则 2 时,Bi 只是 Number

  • List<? extends Number>不能抄送 List<String>
  • ?可以按规则 2 CC,但由于类型不兼容而导致编译时错误


  • 现在让我们尝试一些嵌套:
  • List<List<?>>不能抄送 List<List<String>>
  • 规则 4 适用,并且 CC 不是递归的,所以 ?不能抄送

  • List<? extends List<?>>可以抄送List<List<String>>
  • 第一 ?可以按规则 2 抄送
  • 在应用规则 2 时,Bi 现在是 List<?> , 可以抄送 List<String>
  • 两者 ?可以抄送

  • List<? extends List<? extends Number>>可以抄送List<List<Integer>>
  • 第一?可以按规则 2 抄送
  • 在应用规则 2 时,Bi 现在是 List<? extends Number> , 可以抄送 List<Integer>
  • 两者 ?可以抄送

  • List<? extends List<? extends Number>>不能抄送 List<List<Integer>>
  • 第一?可以按规则 2 抄送
  • 在应用规则 2 时,Bi 现在是 List<? extends Number> ,它可以 CC,但在应用于 List<Integer> 时会出现编译时错误
  • 两者 ?可以抄送


  • 进一步说明为什么有些?可以 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!
    
    原因WildSnippet2编译是因为,如上所述,没有 ?可以抄送。在 WildSnippet1 , 要么 KV (或两者)HashMap<K,V> can CC,通过new直接实例化非法的。

    关于java - 泛型方法上的多个通配符使 Java 编译器(和我!)非常困惑,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3546745/

    相关文章:

    Elasticsearch 通配符搜索和相关性

    java - 输出打印 boolean 值而不是字符串

    java - 如何创建右侧包含用于在左侧显示的选项卡的 SplitPane?

    Java:可以列出实现更多接口(interface)的类

    Java 泛型 - Class<?> 构造函数参数问题

    c++ - Boost.MultiIndex 数据结构中的通配符搜索?

    java - 命令行参数中 * 的问题

    Java console applications : Is System. 出来还有办法吗?

    java - Spring MVC : Appropriate extension point for wrapping API Responses

    c# - 将 String 转换为 Type,其中 Type 是一个变量