【问题标题】:What is the difference between <? extends Base> and <T extends Base>?< 和有什么区别?扩展基础> 和 <T 扩展基础>?
【发布时间】:2020-06-17 13:23:57
【问题描述】:

在这个例子中:

import java.util.*;

public class Example {
    static void doesntCompile(Map<Integer, List<? extends Number>> map) {}
    static <T extends Number> void compiles(Map<Integer, List<T>> map) {}

    static void function(List<? extends Number> outer)
    {
        doesntCompile(new HashMap<Integer, List<Integer>>());
        compiles(new HashMap<Integer, List<Integer>>());
    }
}

doesntCompile() 编译失败:

Example.java:9: error: incompatible types: HashMap<Integer,List<Integer>> cannot be converted to Map<Integer,List<? extends Number>>
        doesntCompile(new HashMap<Integer, List<Integer>>());
                      ^

compiles() 被编译器接受。

This answer 解释说,唯一的区别是,与&lt;? ...&gt; 不同,&lt;T ...&gt; 允许您稍后引用该类型,但似乎并非如此。

在这种情况下&lt;? extends Number&gt;&lt;T extends Number&gt; 有什么区别,为什么第一个编译不出来?

【问题讨论】:

标签: java dictionary generics arraylist jvm


【解决方案1】:

通过使用以下签名定义方法:

static <T extends Number> void compiles(Map<Integer, List<T>> map) {}

并像这样调用它:

compiles(new HashMap<Integer, List<Integer>>());

您正在将 T 与您提供的类型相匹配。

在 jls §8.1.2 中,我们发现(有趣的部分由我加粗):

泛型类声明定义了一组参数化类型(第 4.5 节),类型参数对类型参数部分的每个可能调用都有一个。所有这些参数化类型在运行时共享同一个类。

换句话说,T 类型与输入类型匹配并分配了Integer。签名将有效地变为static void compiles(Map&lt;Integer, List&lt;Integer&gt;&gt; map)

说到doesntCompile方法,jls定义了子类型化规则(§4.5.1,我加粗了):

如果 T2 表示的类型集在自反和传递闭包下可证明是 T1 表示的类型集的子集,则称一个类型参数 T1 包含另一个类型参数 T2,写作 T2

  • ?扩展 T 则扩展 S

  • ?扩展 T

  • ?超级 T

  • ?超级 T

  • ?超级 T

  • T

  • T

  • T

这意味着,? extends Number 确实包含Integer,甚至List&lt;? extends Number&gt; 包含List&lt;Integer&gt;,但Map&lt;Integer, List&lt;? extends Number&gt;&gt;Map&lt;Integer, List&lt;Integer&gt;&gt; 并非如此。有关该主题的更多信息,请访问in this SO thread。您仍然可以通过声明您期望List&lt;? extends Number&gt; 的子类型来使带有? 通配符的版本工作:

public class Example {
    // now it compiles
    static void doesntCompile(Map<Integer, ? extends List<? extends Number>> map) {}
    static <T extends Number> void compiles(Map<Integer, List<T>> map) {}

    public static void main(String[] args) {
        doesntCompile(new HashMap<Integer, List<Integer>>());
        compiles(new HashMap<Integer, List<Integer>>());
    }
}

【讨论】:

  • [1] 我认为您的意思是 ? extends Number 而不是 ? extends Numeric。 [2] 您关于 "不是 List extends Number> 和 List" 的断言是不正确的。正如@VinceEmigh 已经指出的那样,您可以创建一个方法static void demo(List&lt;? extends Number&gt; lst) { } 并像这样调用它demo(new ArrayList&lt;Integer&gt;()); 或这个demo(new ArrayList&lt;Float&gt;());,代码编译并运行正常。还是我可能误读或误解了您所说的内容?
  • @你在这两种情况下都是对的。关于你的第二点,我以一种误导的方式写了它。我的意思是 List&lt;? extends Number&gt; 作为整个地图的类型参数,而不是它本身。非常感谢您的评论。
  • @skomisa 出于同样的原因List&lt;Number&gt; 不包含List&lt;Integer&gt;。假设你有一个函数static void check(List&lt;Number&gt; numbers) {}。当使用check(new ArrayList&lt;Integer&gt;()); 调用时,它不会编译,您必须将方法定义为static void check(List&lt;? extends Number&gt; numbers) {}。与地图相同,但嵌套更多。
  • @skomisa 就像Number 是list 的类型参数,需要加上? extends 使其协变,List&lt;? extends Number&gt;Map 的类型参数,也需要@987654355 @ 表示协方差。
  • 好的。由于您提供了多级通配符(又名“嵌套通配符”?)的解决方案,并链接到相关的 JLS 参考资料,因此获得了赏金。
【解决方案2】:

在通话中:

compiles(new HashMap<Integer, List<Integer>>());

T 与 Integer 匹配,因此参数的类型是 Map&lt;Integer,List&lt;Integer&gt;&gt;。方法doesntCompile 的情况并非如此:无论调用中的实际参数是什么,参数的类型都保持Map&lt;Integer, List&lt;? extends Number&gt;&gt;;这不能从HashMap&lt;Integer, List&lt;Integer&gt;&gt; 分配。

更新

doesntCompile 方法中,没有什么能阻止你做这样的事情:

static void doesntCompile(Map<Integer, List<? extends Number>> map) {
    map.put(1, new ArrayList<Double>());
}

很明显,它不能接受HashMap&lt;Integer, List&lt;Integer&gt;&gt; 作为参数。

【讨论】:

  • 那么对doesntCompile 的有效调用会是什么样子呢?只是好奇。
  • @XtremeBiker doesntCompile(new HashMap&lt;Integer, List&lt;? extends Number&gt;&gt;()); 会起作用,doesntCompile(new HashMap&lt;&gt;()); 也会起作用。
  • @XtremeBiker,即使这样也行,Map> map = new HashMap>(); map.put(null, new ArrayList());不编译(地图);
  • "that is notassignable from HashMap&lt;Integer, List&lt;Integer&gt;&gt;" 你能详细说明为什么它不能从它分配吗?
  • 这和static void demo(List&lt;? extends Number&gt; list)有什么区别?你也不能在里面做list.add(new Double(0.0)),但是它不能在demo里面编译,而不是像doesntCompile那样在外面编译。
【解决方案3】:

演示的简单示例。同样的例子可以如下图所示。

static void demo(List<Pair<? extends Number>> lst) {} // doesn't work
static void demo(List<? extends Pair<? extends Number>> lst) {} // works
demo(new ArrayList<Pair<Integer>()); // works
demo(new ArrayList<SubPair<Integer>()); // works for subtype too

public static class Pair<T> {}
public static class SubPair<T> extends Pair<T> {}

List&lt;Pair&lt;? extends Number&gt;&gt; 是多级通配符类型,而List&lt;? extends Number&gt; 是标准通配符类型。

通配符类型List&lt;? extends Number&gt; 的有效具体实例包括NumberNumber 的任何子类型,而在List&lt;Pair&lt;? extends Number&gt;&gt; 的情况下,它是类型参数的类型参数并且本身具有泛型类型的具体实例.

泛型是不变的,所以Pair&lt;? extends Number&gt; 通配符类型只能接受Pair&lt;? extends Number&gt;&gt;。内部类型? extends Number 已经是协变的。您必须将封闭类型设为协变以允许协变。

【讨论】:

  • 为什么&lt;Pair&lt;Integer&gt;&gt; 不能与&lt;Pair&lt;? extends Number&gt;&gt; 一起使用,但可以与&lt;T extends Number&gt; &lt;Pair&lt;T&gt;&gt; 一起使用?
  • @jaco0646 您本质上是在问与 OP 相同的问题,the answer from Andronicus 已被接受。请参阅该答案中的代码示例。
  • @skomisa,是的,我问同样的问题有几个原因:一个是这个答案实际上似乎并没有解决 OP 的问题;但二是我觉得这个答案更容易理解。我无法以任何方式遵循 Andronicus 的答案,让我理解嵌套与非嵌套泛型,甚至 T?。部分问题在于,当 Andronicus 到达他的解释的关键点时,他会顺从另一个只使用琐碎示例的线程。我希望在这里得到一个更清晰、更完整的答案。
  • @jaco0646 好的。 Angelika Langer 的文档“Java 泛型常见问题解答 - 类型参数” 有一个标题为 What do multi-level (i.e., nested) wildcards mean? 的常见问题解答。这是我所知道的用于解释 OP 问题中提出的问题的最佳来源。嵌套通配符的规则既不简单也不直观。
【解决方案4】:

我建议你看看documentation of generic wildcards,尤其是guidelines for wildcard use

坦率地说你的方法#doesntCompile

static void doesntCompile(Map<Integer, List<? extends Number>> map) {}

然后像打电话一样

doesntCompile(new HashMap<Integer, List<Integer>>());

根本不正确

让我们添加合法实施:

    static void doesntCompile(Map<Integer, List<? extends Number>> map) {
        List<Double> list = new ArrayList<>();
        list.add(0.);
        map.put(0, list);
    }

真的很好,因为Double扩展了Number,所以放List&lt;Double&gt;List&lt;Integer&gt;一样绝对没问题,对吧?

但是,您仍然认为在您的示例中传递 new HashMap&lt;Integer, List&lt;Integer&gt;&gt;()合法吗?

编译器不这么认为,并且正在尽力避免这种情况。

尝试使用方法#compile 执行相同的实现,编译器显然不允许您将双精度列表放入映射中。

    static <T extends Number> void compiles(Map<Integer, List<T>> map) {
        List<Double> list = new ArrayList<>();
        list.add(10.);
        map.put(10, list); // does not compile
    }

基本上你只能放List&lt;T&gt;,这就是为什么用 new HashMap&lt;Integer, List&lt;Integer&gt;&gt;()new HashMap&lt;Integer, List&lt;Double&gt;&gt;()new HashMap&lt;Integer, List&lt;Long&gt;&gt;()new HashMap&lt;Integer, List&lt;Number&gt;&gt;()

因此,简而言之,您是在尝试使用编译器作弊,它可以公平地防御这种作弊。

注意:Maurice Perry 发布的答案绝对正确。我只是不确定它是否足够清楚,所以尝试(真的希望我设法)添加更广泛的帖子。

【讨论】: