泛型、类型参数和通配符
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<?>
是一种类型(而?
不是)。所以List<List<?>>
表示List
,其元素的类型为List<?>
,而List<?>
表示List
某种未知类型。
是的,这里错误的性质是:如果你有一个List<?>
?
可以像类型(捕获)一样处理第一个例子。但是如果你有一些像Map<?, List<?>>
这样的嵌套类型,则只能捕获“最外层”类型中的通配符(因此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<T>
,你传入一个List<?>
;这(本质上)是List<Object>
。 T
可以分配给Object
类型,编译器很高兴。
2
你的泛型方法是一样的,你传入一个List<List<?>>
。 T
可以分配给List<?>
类型,编译器又高兴了。
3
这与 2 基本相同,只是多了一层嵌套。 T
仍然是 List<?>
类型。
4
这是一个有点梨形的地方,也是我从上面的观点进来的地方。
您的通用方法采用List<List<T>>
。你传入一个List<List<?>>
。现在,由于泛型类型不是协变的,List<?>
不能分配给 List<T>
。
实际的编译器错误(Java 8)是:
必填:
java.util.List<java.util.List<T>>
找到:java.util.List<java.util.List<?>>
原因:无法推断 类型变量T
(参数不匹配;java.util.List<java.util.List<?>>
无法转换为java.util.List<java.util.List<T>>
)
基本上,编译器告诉您它找不到要分配的T
,因为必须推断嵌套在外部列表中的List<T>
的类型。
让我们更详细地看一下:
List<?>
是一个未知类型的List
- 它可以是List<Integer>
或List<String>
;我们可以将get
设为Object
,但我们不能add
。因为否则我们会遇到我提到的协方差问题。
List<List<?>>
是 List
或 List
的某个未知类型 - 它可能是 List<List<Integer>>
或 List<List<String>>
。在 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<List<Integer>>
分配给任何其他通用List
。
【讨论】:
这几乎让我信服。当您说 T 可以分配给 Object 类型时,唯一的问题是在示例 1 中。这对我来说似乎不对 - 如果您用采用 List 这里也一样 - 听起来很有说服力,但......并不完全:您可以在显式传入List<List<Object>>
时调用最后一个方法,因此该参数不应该是指缺少协方差,而是类型推断系统的弱点和其中涉及的通配符....
你不是说objects.add(1);
吗?
@pbabcdefp 添加了一个澄清的编辑 - 希望能更清楚地解释这个问题。
一些被阻止的事情没有你提到的例外的风险。例如。如果我有一个带有签名static <T> List<List<T>> m1(List<T> a)
的方法和另一个带有签名static <T> void m2(List<List<T>> a)
的方法,您会发现如果x
的类型为List<?>
,那么m2(m1(x))
将无法编译!返回 List<List<T>>
的方法返回的值不被接受为参数类型为 List<List<T>>
的方法的参数。但是m2(m1(x))
是没有风险的——你可以编写一个带有参数List<T>
的方法,它的主体是m2(m1(x));
,它接受List<?>
就好了。以上是关于泛型、类型参数和通配符的主要内容,如果未能解决你的问题,请参考以下文章