泛型、类型参数和通配符

Posted

技术标签:

【中文标题】泛型、类型参数和通配符【英文标题】:Generics, Type Parameters and Wildcards 【发布时间】:2014-12-14 01:10:12 【问题描述】:

我正在尝试理解 Java 泛型,但它们似乎非常难以理解。比如这样就可以了……

public class Main 

    public static void main(String[] args) 
        List<?> list = null;
        method(list);
    

    public static <T> void method(List<T> list)  

……原来如此……

public class Main 

    public static void main(String[] args) 
        List<List<?>> list = null;
        method(list);
    

    public static <T> void method(List<T> list)  

...还有这个...

public class Main 

    public static void main(String[] args) 
        List<List<List<?>>> list = null;
        method(list);
    

    public static <T> void method(List<List<T>> list)  

...但这不能编译:

public class Main 

    public static void main(String[] args) 
        List<List<?>> list = null;
        method(list);
    

    public static <T> void method(List<List<T>> list)  

有人可以用简单的语言解释发生了什么吗?

【问题讨论】:

只是好奇,鲍里斯的回答是否足够,或者您对此仍然持怀疑态度? @Radiodef 我现在对此一点也不怀疑。花了一段时间,但我终于明白了。自从我写这个问题以来,我一直在回答大量的泛型问题! 好的,没问题。 ; ) 我看到了 cmets 并考虑写一些额外的东西。这种情况并不经常出现。 "Capture conversion is not applied recursively." 只能捕获第一个示例中的通配符。 @Radiodef 对我来说,关键是当我终于明白List&lt;?&gt; 是一种类型(而? 不是)。所以List&lt;List&lt;?&gt;&gt; 表示List,其元素的类型为List&lt;?&gt;,而List&lt;?&gt; 表示List 某种未知类型。 是的,这里错误的性质是:如果你有一个List&lt;?&gt; ? 可以像类型(捕获)一样处理第一个例子。但是如果你有一些像Map&lt;?, List&lt;?&gt;&gt; 这样的嵌套类型,则只能捕获“最外层”类型中的通配符(因此Map 而不是List)可以被捕获。 Boris 的回答说明了 why(我们可以做不安全的事情),但没有说明 how(捕获)的细节。通配符的语义略有不同,具体取决于它是“外部”还是“内部”类型。 【参考方案1】:

了解泛型类型的主要内容是它们不是协变的。

所以虽然你可以这样做:

final String string = "string";
final Object object = string;

以下将无法编译:

final List<String> strings = ...
final List<Object> objects = strings;

这是为了避免绕过泛型类型的情况:

final List<String> strings = ...
final List<Object> objects = strings;
objects.add(1);
final String string = strings.get(0); <-- oops

所以,一个一个地浏览你的例子

1

你的泛型方法需要一个List&lt;T&gt;,你传入一个List&lt;?&gt;;这(本质上)是List&lt;Object&gt;T 可以分配给Object 类型,编译器很高兴。

2

你的泛型方法是一样的,你传入一个List&lt;List&lt;?&gt;&gt;T 可以分配给List&lt;?&gt; 类型,编译器又高兴了。

3

这与 2 基本相同,只是多了一层嵌套。 T 仍然是 List&lt;?&gt; 类型。

4

这是一个有点梨形的地方,也是我从上面的观点进来的地方。

您的通用方法采用List&lt;List&lt;T&gt;&gt;。你传入一个List&lt;List&lt;?&gt;&gt;。现在,由于泛型类型不是协变的,List&lt;?&gt; 不能分配给 List&lt;T&gt;

实际的编译器错误(Java 8)是:

必填:java.util.List&lt;java.util.List&lt;T&gt;&gt; 找到: java.util.List&lt;java.util.List&lt;?&gt;&gt;原因:无法推断 类型变量T (参数不匹配;java.util.List&lt;java.util.List&lt;?&gt;&gt; 无法转换为java.util.List&lt;java.util.List&lt;T&gt;&gt;

基本上,编译器告诉您它找不到要分配的T,因为必须推断嵌套在外部列表中的List&lt;T&gt; 的类型。

让我们更详细地看一下:

List&lt;?&gt; 是一个未知类型List - 它可以是List&lt;Integer&gt;List&lt;String&gt;;我们可以将get 设为Object但我们不能add。因为否则我们会遇到我提到的协方差问题。

List&lt;List&lt;?&gt;&gt;ListList 的某个未知类型 - 它可能是 List&lt;List&lt;Integer&gt;&gt;List&lt;List&lt;String&gt;&gt;。在 1 的情况下,可以将 T 分配给 Object 并且不允许在通配符列表上进行 add 操作。如果 4 无法做到这一点 - 主要是因为没有泛型构造来防止 add 到外部 List

如果编译器在第二种情况下将 T 分配给 Object,则可能会出现以下情况:

final List<List<Integer>> list = ...
final List<List<?>> wildcard = list;
wildcard.add(Arrays.asList("oops"));

因此,由于协方差,不可能安全地将List&lt;List&lt;Integer&gt;&gt; 分配给任何其他通用List

【讨论】:

这几乎让我信服。当您说 T 可以分配给 Object 类型时,唯一的问题是在示例 1 中。这对我来说似乎不对 - 如果您用采用 List 的非泛型方法替换该方法,它就不起作用。当然,在示例 1 中,编译器能够看到类型 T 可以将 List> 与 List 匹配。为什么编译器在示例 4 中看不到相同的内容? 这里也一样 - 听起来很有说服力,但......并不完全:您可以在显式传入 List&lt;List&lt;Object&gt;&gt; 时调用最后一个方法,因此该参数不应该是指缺少协方差,而是类型推断系统的弱点和其中涉及的通配符.... 你不是说objects.add(1);吗? @pbabcdefp 添加了一个澄清的编辑 - 希望能更清楚地解释这个问题。 一些被阻止的事情没有你提到的例外的风险。例如。如果我有一个带有签名static &lt;T&gt; List&lt;List&lt;T&gt;&gt; m1(List&lt;T&gt; a) 的方法和另一个带有签名static &lt;T&gt; void m2(List&lt;List&lt;T&gt;&gt; a) 的方法,您会发现如果x 的类型为List&lt;?&gt;,那么m2(m1(x)) 将无法编译!返回 List&lt;List&lt;T&gt;&gt; 的方法返回的值不被接受为参数类型为 List&lt;List&lt;T&gt;&gt; 的方法的参数。但是m2(m1(x)) 是没有风险的——你可以编写一个带有参数List&lt;T&gt; 的方法,它的主体是m2(m1(x));,它接受List&lt;?&gt; 就好了。

以上是关于泛型、类型参数和通配符的主要内容,如果未能解决你的问题,请参考以下文章

Java泛型中T和问号(通配符)的区别

Java泛型和通配符那点事

JAVA泛型_泛型类接口通配符方法上下边界

Java泛型中使用通配符或有界类型参数是啥?

计算机程序的思维逻辑 (36) - 泛型 (中) - 解析通配符

Java泛型类型通配符 可变参数