泛型、类型参数和通配符

21

我正在尝试理解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) { }
}

有人能用简单的语言解释一下正在发生什么吗?

只是好奇,Boris的回答足够了吗?还是你对此仍然有些犹豫? - Radiodef
1
@Radiodef,我现在对此非常确定。虽然花了一些时间,但我终于明白了。自从我写下这个问题以来,我已经回答了很多关于泛型的问题! - Paul Boddington
1
好的,没问题。;)我看到了评论并考虑写一些额外的东西。这种情况并不经常发生。“捕获转换不递归应用。” 只有第一个示例中的通配符可以被捕获。 - Radiodef
@Radiodef 对我来说关键是当我终于明白List<?>是一种类型(而?不是)。因此,List<List<?>>表示一个元素类型为List<?>List,而List<?>表示某种未知类型的List - Paul Boddington
1
是的,这里错误的本质在于:如果你有一个 List<?>,则可以像第一个示例中那样将 ? 视为类型(捕获)。但是,如果你有一些嵌套类型,例如 Map<?, List<?>>,只有最外层类型中的通配符(因此是 Map 而不是 List)可以被捕获。Boris 的答案展示了 为什么(我们可以做不安全的事情),但没有涉及 如何(捕获)。通配符在“外部”或“内部”类型中具有略微不同的语义。 - Radiodef
1个回答

14

理解泛型的主要事情是,它们不是协变的。

因此,尽管您可以执行以下操作:

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>;我们可以从中获取 Object,但我们不能添加。否则,我们就会遇到我提到的协变问题。 List<List<?>>是一个未知类型的List,它可能是List<List<Integer>>List<List<String>>。在情况1中,可以将T分配给Object,并且只允许在通配符列表上执行不带add操作。在情况4中,这是不可能的-主要是因为没有一种泛型构造来防止对外部List进行add操作。
如果编译器在第二种情况下将T分配给Object,那么可能会出现以下类似的情况:
final List<List<Integer>> list = ...
final List<List<?>> wildcard = list;
wildcard.add(Arrays.asList("oops"));

因为协变性,不能安全地将List<List<Integer>>分配给任何其他泛型List

这几乎让我信服了。唯一的争议在于示例1中,当您说T可以分配给Object类型时。我觉得这不对-如果您将该方法替换为接受List<Object>的非泛型方法,则无法正常工作。当然,在示例1中,编译器能够看到可以找到一个类型T来将List<?>匹配到List<T>。为什么编译器不能在示例4中看到相同的事情呢? - Paul Boddington
2
你的意思是 objects.add(1); 吗? - Ryan Dougherty
有些被阻止的事情是没有你提到的异常风险的。例如,如果我有一个签名为 static <T> List<List<T>> m1(List<T> a) 的方法和另一个签名为 static <T> void m2(List<List<T>> a) 的方法,你会发现如果 xList<?> 类型,则 m2(m1(x)) 无法编译!返回 List<List<T>> 的方法的值不能作为类型为 List<List<T>> 的参数传递给方法。但是 m2(m1(x)) 是无风险的 - 你可以编写一个带有参数 List<T> 的方法,其主体为 m2(m1(x)); 并且它可以很好地接受 List<?> - Paul Boddington
实际上我错了... m2(m1(x)) 可以正常执行。但 IntelliJ 不知道它会执行,所以发出错误消息 method2(java.util.List<T>) 无法应用于 method2(java.util.List<?>)。如果 IntelliJ 的作者们都不理解这些东西,那么我们其他人还有什么希望呢? - Paul Boddington
@pbabcdefp 我使用IntelliJ - 它在类型推断方面确实存在一些问题。我建议您将其记录为错误 - 帮助社区! - Boris the Spider
显示剩余2条评论

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