【问题标题】:Difference of assignability with nested wildcards in Java 7/8 genericsJava 7/8 泛型中嵌套通配符的可分配性差异
【发布时间】:2023-03-04 13:54:01
【问题描述】:

以下在 JDK8 中编译得很好,但在 JDK7 中会出现 incompatible types 错误。

List<List<? extends Number>> xs = Arrays.asList(Arrays.asList(0));

根据this answerList&lt;List&lt;? extends Number&gt;&gt;List&lt;List&lt;Integer&gt;&gt; 没有超类型关系。

Java 8 中的哪些变化使这项任务有效?我也很难理解为什么它在 Java 7 中不起作用


这两个语句都使用 JDK7 编译,没有类型错误:

List<? extends Number> xs = Arrays.asList(0);
List<? extends List<? extends Number>> ys = Arrays.asList(Arrays.asList(0));

在我看来,这两种方法都适用于 JDK7,但上面的原始示例却没有。当然,它们都可以在 JDK8 中工作。我认为要真正了解这里发生了什么,我需要了解为什么这些示例在 Java 7 中是合法的,但原始示例却不是。

【问题讨论】:

  • 在我看来,因为Arrays.asList(0) 会返回一个List&lt;Integer&gt;,而Arrays.asList() 会返回一个List,其元素是List&lt;Integer&gt;,在我看来,分配@987654331 @ 实际上是对的......不过我几乎肯定会误解一些东西
  • @SotiriosDelimanolis Aaaa 原来我在试验时忘记将线路改回来。我也会失败。
  • @user3580294 - "... 在我看来,分配 List> 实际上是正确的..." 这正是我的意思米糊涂了。 Java 8 的行为对我来说似乎是直观正确的行为,所以我不明白为什么 Java 7 中会出现类型错误。这背后的基本原理是什么?显然他们决定在 Java 8 中修复它,但我不知道他们究竟做了什么改变才能使它工作。
  • @DaoWen 我听说 JLS 对 Java 8 中类型推断/检测/检查的工作方式进行了改进,因此规范可能不允许它返回然后。我不能肯定地说。一个很有趣的问题....
  • Java 7 的正确习惯用法是List&lt;List&lt;? extends Number&gt;&gt; ys = Arrays.&lt;List&lt;? extends Number&gt;asList(Arrays.asList(0));。它表明即使在 Java 7 下,赋值也是正确的,并且只是有限的 type-in​​ference 问题。

标签: java generics java-8 bounded-wildcard


【解决方案1】:

我认为这与invocation contextswidening reference conversion 有关。

基本上,在该调用上下文中,Arrays.asList(0) 中的参数0 的类型可以装箱为Integer,然后扩大为Number。发生这种情况时,Arrays.asList(0) 的返回类型为 List&lt;Number&gt;。通过相同的过程,List&lt;Number&gt; 可以在用作外部Arrays.asList(..) 的参数之前转换为List&lt;? extends Number&gt;

这相当于在 Java 7 中使用显式类型参数

List<List<? extends Number>> xs = Arrays.<List<? extends Number>>asList(Arrays.<Number>asList(0));

在 Java 7 中,表达式的类型是相同的,无论它在哪里使用,无论是独立表达式还是用于赋值表达式。

Java 8 引入了poly-expressions,其中表达式的类型可能会受到表达式的目标类型的影响。

例如在下面的赋值表达式中

List<Number> numbers = Arrays.asList(1);

表达式Arrays.asList(1) 的类型是被调用方法的返回类型,它完全取决于泛型类型参数。在这种情况下,该类型参数将被推断为Integer,因为值1 可以通过装箱转换转换为Integer(基元不能与泛型一起使用)。所以表达式的类型是List&lt;Integer&gt;

在 Java 7 中,此赋值表达式无法编译,因为无法将 List&lt;Integer&gt; 分配给 List&lt;Number&gt;。这可以通过在调用 asList 时提供显式类型参数来解决

List<Number> numbers = Arrays.<Number>asList(1);

在这种情况下,方法调用需要一个 Number 参数作为其第一个参数,而值 1 满足这一要求。

在 Java 8 中,赋值表达式是 poly expression

一个方法调用表达式是一个多边形表达式,如果所有的 以下是正确的:

  • 调用出现在赋值上下文或调用上下文(§5.2、§5.3)中。

  • 如果调用是合格的(即除第一个以外的任何形式的MethodInvocation),则调用会在标识符左侧省略TypeArguments。 p>

  • 由以下小节确定的要调用的方法是泛型的(第 8.4.4 节),并且具有提及至少一个方法类型参数的返回类型。

作为一个 poly 表达式,它可能会受到分配给它的变量类型的影响。这就是发生的事情。泛型类型Number 影响在调用Arrays.asList(1) 时推断的类型参数。

注意它在下面的例子中是如何工作的

List<Number> numbers = ...;
List<Integer> integers = ...; // integers is not a poly expression
numbers = integers; // nope

所以它不是协方差,但我们在某些地方得到了它的一些好处。

【讨论】:

  • 您到底是如何找到埋在 JLS 中的这些东西的?
  • @user3580294 我最近阅读了 Angelika Langer 的一篇文章的一部分,该文章谈到了调用上下文(尽管我认为那是在它们应用于 Java 8 之前)。我记得这个词,只是ctrl+f'ed。
  • 我没有看到您在 Java 7 和 8 之间链接的部分有任何显着差异:(Java 7) Method Invocation Conversion(Java 7) Widening Reference Conversion。另请注意,我已经用更多示例更新了我的问题。
  • @Dao 那我还要加this。基本上,该调用是一个 poly 表达式,其中表达式的一个 poly 表达式 can be influenced by the target type
  • @DaoWen 我现在正在工作,但如果您没有想要的答案,我稍后再回复。
【解决方案2】:

很简单:

在 Java 7 中,推断类型参数时不考虑方法调用的上下文。唯一考虑方法调用的参数的地方。

在您的情况下,int 被装箱到 Integer 从而产生 List&lt;Integer&gt; 作为内部调用的类型,然后 List&lt;List&lt;Integer&gt;&gt; 作为外部调用的类型。现在你遇到了一个问题,因为你想将结果分配给List&lt;List&lt;? extends Number&gt;&gt; 类型的变量,这是不可能的,因为只要没有使用通配符,泛型就是不变的,即List&lt;X&gt; 永远不能转换为List&lt;Y&gt;。在您的情况下,XList&lt;Integer&gt;YList&lt;? extends Number&gt;。即使List&lt;? extends Number&gt; 包含通配符,它​​本身也不使用通配符,即它不是? extends List&lt;? extends Number&gt;。这就是它不能在 Java 7 中编译的原因。

我知道理解泛型、变体和通配符并不容易。也许我可以这样为你澄清:

  1. 通常,A&lt;Y&gt; 永远不会被视为任何A&lt;Z&gt; 的子类型。
  2. 但是,如果Z 继承自Y,则A&lt;Z&gt;A&lt;? extends Y&gt; 的子类型。
  3. 现在到嵌套泛型:我们知道,内部类型(A&lt;Z&gt;A&lt;? extends Y&gt; 在子类型关系中相关。但是如果我们将它们包装在另一个泛型类型中,例如 B&lt;A&lt;Z&gt;&gt;B&lt;A&lt;? extends Y&gt;&gt;那么规则1适用:由于没有通配符,第二个B不被认为是第一个的子类型。如果我们再次引入通配符,那么它们是:B&lt;A&lt;Z&gt;&gt;确实是B&lt;? extends A&lt;? extends Y&gt;&gt;的子类型。但现在请注意,您的示例中缺少外部 ? extends,因此它无法在 Java 7 中编译。

现在到 Java 8。Java 8 在推断类型参数时也会考虑调用的上下文。因此,Java 8 认为您希望将Arrays.asList 调用的结果传递给List&lt;List&lt;? extends Number&gt;&gt; 类型的变量。因此它试图找到使这个赋值合法的类型参数。然后推断内部调用的类型参数必须是Number,否则赋值将不合法。

简而言之:Java 8 在选择类型参数时比 Java 7 聪明得多,因为它还会查看上下文,而不仅仅是参数。

【讨论】:

  • List&lt;Integer&gt; 不是 List&lt;? extends Number&gt; 吗?我认为通配符允许这样的协方差。
  • List&lt;? extends Number&gt; xs = Arrays.asList(0); 使用 JDK7 编译没有错误。这和你的回答不矛盾吗? (请注意,我已经用这个例子更新了我的问题。)
  • @user3580294:他们做到了!但是通配符不具有传递性。仅仅因为 List&lt;Integer&gt;List&lt;? extends Number&gt; 并不意味着 List>` 是 List&lt;List&lt;? extends Number&gt;&gt;
  • @DaoWen:看我上面的评论。我已经澄清了我的回答。问题在于嵌套泛型。
  • 啊,原来如此。看起来我把列表弄反了。谢谢!
猜你喜欢
  • 2011-09-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-21
相关资源
最近更新 更多