【问题标题】:Conditions for Method Reference Expression to be "exact"方法引用表达式“精确”的条件
【发布时间】:2022-02-28 15:26:44
【问题描述】:

考虑以下来自 JLS (§15.13.1) 的文章

如果满足以下所有条件,则以 Identifier 结尾的方法引用表达式是精确的:

  • 如果方法引用表达式的格式为 ReferenceType ::[TypeArguments] Identifier,则 ReferenceType 不表示原始类型。
  • 要搜索的类型只有一个名为 Identifier 的成员方法,该方法引用表达式所在的类或接口可以访问该方法。
  • 此方法不是可变参数 (§8.4.1)。
  • 如果此方法是通用方法(第 8.4.4 节),则方法引用表达式提供 类型参数。

考虑以下代码sn-p:

class Scratch {

  public static void main(String[] args) {
    Scratch.funct(new ImplementingClass()::<Functional1>hitIt);
  }

  public static void funct(Functional1 a){}
  public static void funct(Functional2 a){}
}
interface Functional1 {<T> T hitIt();}
interface Functional2 {<T> T hitIt();}

class ImplementingClass{
  public <T> T hitIt(){return null;}
}

显然 - 这满足了为准确引用方法而提到的所有条件。

不确定为什么在这种特殊情况下方法引用仍然不准确?我在条款中遗漏了什么吗?

解决办法:

根据@Sweeper @DidierL 和@Holger 的输入,我总结了以下内容:

  1. 两个功能接口都有 functionType &lt;T&gt; () -&gt; T
  2. 方法引用…::&lt;Functional1&gt;hitItT 替换为Functional1,因此生成的函数签名为() -&gt; Functional1,与&lt;T&gt; () -&gt; T 不匹配。

【问题讨论】:

  • 是什么让您认为它不准确?
  • 编译时 - 我收到错误消息 -: reference to funct is ambiguous both method funct(Functional1) in Scratch and method funct(Functional2) in Scratch match
  • 我使用这个参考来了解编译器给出的不精确方法的症状-:blog.gilliard.lol/2017/10/23/…
  • 请注意,像&lt;T&gt; T hitIt() 这样声明的方法确实是个坏主意,因为调用方不会进行编译时检查。它基本上说“我会猜测并返回调用者期望的任何东西,相信我”。几年前I faced an issue 在将应用程序从 Java 7 迁移到 8 时,使用类似的方法导致调用方调用了不同的、不相关的方法。而且我从来没有找到一种通用的方法来识别这种情况。
  • 在您与@Sweeper 在他们(现已删除)的回答下进行的讨论之后,很明显,由于Functional1/Functional2 中的&lt;T&gt; T 方法声明,这不应该编译,但是更改返回类型应该使它编译而不是编译。你能相应地更新你的问题吗?在问题本身中添加编译器错误也是值得的。

标签: java generics type-inference method-reference jls


【解决方案1】:

首先是一个警告:IANAJL (IANAL for Java ?)

据我所知,如果您将两个接口方法设为非泛型,这应该可以编译,但事实并非如此。让我们尽可能简化代码以重现问题:

class Scratch {
  public static void main(String[] args) {
    Scratch.funct(ImplementingClass::<Void>hitIt);
  }

  public static void funct(Functional1 a){}
  public static void funct(Functional2 a){}
}
interface Functional1 {Integer hitIt();}
interface Functional2 {String hitIt();}

class ImplementingClass{
  public static <T> Integer hitIt(){return null;}
}

简化:

  • 这两个接口现在具有非泛型方法
  • ImplementingClass.hitIt() 现在是静态的并且有一个具体的返回类型(非泛型)

现在让我们分析调用以检查它是否应该编译。我放了 Java 8 规范的链接,但它们在 17 中非常相似。

15.12.2.1. Identify Potentially Applicable Methods

当且仅当满足以下所有条件时,成员方法才可能适用于方法调用:

[…]

  • 如果成员是一个固定数量的方法,数量为n,则方法调用的数量等于n,并且对于所有i (1 ≤ in),方法调用的第 i 个参数可能兼容,如下定义,使用方法的第 i 个参数的类型。

[…]

根据以下规则,表达式可能与目标类型兼容

[…]

  • 方法引用表达式 (§15.13) 可能与函数式接口类型兼容,如果类型的函数类型数量为 n,则方法引用至少存在一种可能适用的方法具有 arity n (§15.13.1) 的表达式,并且以下条件之一为真:
    • 方法引用表达式的格式为 ReferenceType :: [TypeArguments] Identifier 并且至少一个可能适用的方法是 i) static 并支持 arity n,或者ii) 不是static 并且支持arity n-1。
    • 方法引用表达式具有其他形式,并且至少有一个可能适用的方法不是static

(最后一个项目符号适用于方法引用使用构造函数调用表达式的问题,即Primary

此时,我们只检查方法引用的数量,因此funct() 两种方法都可能适用。

15.12.2.2. Phase 1: Identify Matching Arity Methods Applicable by Strict Invocation

参数表达式被认为与可能适用的方法 m 的适用性相关,除非它具有以下形式之一:

[…]

  • 不精确的方法引用表达式 (§15.13.1)。

[…]

这是此列表中唯一可能匹配的要点,但是,正如问题中所指出的,我们在此处有一个 exact 方法引用表达式。请注意,如果您删除 &lt;Void&gt;,这将使其成为不精确的方法引用,并且根据下一节,这两种方法都应该适用:

m 成为一个潜在适用的方法(§15.12.2.1),具有 arity n 和形式参数类型 F1 ... @987654348 @n,让e1, ..., en 为实际的参数表达式的方法调用。那么:

[…]

  • 如果m 不是泛型方法,则m 适用于严格调用如果,对于1 ≤ inei 在严格的调用上下文中与Fiei 兼容,与适用性无关。

但是,只有第一个 funct() 方法声明应该适用于严格调用。严格的调用上下文定义为here,但基本上它们检查表达式的类型是否与参数的类型匹配。这里我们的参数类型,方法引用,由15.13.2. Type of a Method Reference部分定义,其相关部分是:

如果 T 是功能接口类型 (§9.8) 并且表达式与 [... ] T.

[…]

如果以下两个都为真,则方法引用表达式与函数类型全等

  • 函数类型标识对应于引用的单个编译时声明。

  • 下列情况之一为真:

    • 函数类型的结果是void
    • 函数类型的结果是 R,并且将捕获转换 (§5.1.10) 应用到所选编译时声明的调用类型 (§15.12.2.6) 的返回类型的结果是 R'(其中 R是可用于推断 R') 的目标类型,并且 R 和 R' 都不是 void,并且 R' 在赋值上下文中与 R 兼容。

这里 R 将是 Integer 用于 Functional1String 用于 Functional2,而 R' 在这两种情况下都是 Integer (因为 ImplementingClass.hitIt() 不需要捕获转换),所以很清楚方法引用与Functional2一致,并且扩展不兼容。

因此,不应考虑通过严格调用将funct(Functional2) 用于适用性,并且由于仅保留funct(Functional1),因此应选择它。

需要注意的是Javac必须在Phase 1中选择这两种方法,因为只有一个Phase可以申请,Phase 2只使用loose context而不是strict,只允许装箱操作,然后第 3 阶段包括可变参数,这也不适用。

如果我们认为 Javac 以某种方式认为方法引用与 Functional2 一致,我看到选择这两种方法的唯一原因是它认为方法引用 与适用性无关上面指定的,我只能在编译器认为它是不精确的方法引用时解释。

15.12.2.5. Choosing the Most Specific Method

这是编译失败的地方。我们应该注意,这里没有任何东西会使编译器选择一种方法而不是另一种方法。适用的规则是:

  • m2 不是泛型的,m1 和 m2 可以通过严格或松散的调用来应用,其中 m1 有形参类型 S1,...,Sn 和 m2 有形参类型 T1,...,Tn,类型对于所有 i (1 ≤ i ≤ n, n = k),对于自变量 ei,Si 比 Ti 更具体。

[…] 对于任何表达式,如果 S <: t href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.10" rel="nofollow" target="_blank">§4.10),类型 S 比类型 T 更具体。

这似乎工作正常:将Functional2 更改为扩展Functional1,它将编译。

如果 T 不是 S 的子类型并且以下条件之一为真(其中 U1 ... Uk 和 R1 是参数类型和捕获S的函数类型的返回类型,V1...Vk和R2是T)的函数类型的参数类型和返回类型:

  • 如果 e 是显式类型的 lambda 表达式 […]
  • 如果 e 是精确方法参考表达式 (§15.13.1),则 i) 对于所有 i (1 ≤ i ≤ k),Ui 与 Vi 相同,并且 ii) 以下其中一项为真:
    • R2 无效。
    • R1 <: r2.>
    • […]

这也不允许消除歧义。但是,将Functional2.hitIt() 更改为返回Number 应该会使Functional1 更具体,因为Integer &lt;: Number

这仍然失败,这似乎证实了编译器不认为它是一个精确的方法引用。

请注意,删除ImplementingClass.hitIt() 中的&lt;T&gt; 允许它编译,而与Functional2.hitIt() 的返回类型无关。有趣的事实:您可以将&lt;Void&gt; 留在调用站点,编译器会忽略它。

更奇怪的是:如果您离开 &lt;T&gt; 并在调用站点添加比所需更多的类型参数,编译器仍然会抱怨模棱两可的调用,而不是类型参数的数量(直到您消除模棱两可)。并不是说这会使方法引用不准确,基于上述定义,但我认为it should be checked first

结论

由于 Eclipse 编译器接受它,我倾向于将其视为 Javac 错误,但请注意,Eclipse 编译器有时在规范方面比 Javac 更宽松,并且已经报告并关闭了一些类似的错误(@ 987654336@, JDK-8170842, ...)。

【讨论】:

  • 为您分析提供了很好的解释-所以您要强调的底线是对于我的问题-理想情况下,编译应该成功,但只是因为 javac 错误,Scratch.funct(new ImplementingClass()::&lt;Functional1&gt;hitIt); 无法唯一地解决功能正确吗?
  • @theutonium.18 根据我的说法,由于确切的方法参考,严格的调用规则应该只选择一种方法,所以对于我的示例来说是的。在您问题的原始代码中,它不应该像 Sweeper 在他们删除的问题中所指出的那样编译。
  • @theutonium.18 我无法重现您描述的行为。您问题中发布的代码无法编译,无论是使用 javac 还是使用 Eclipse。我尝试了不同的版本。甚至不清楚你为什么期望它编译。 Functional1Functional2 都具有相同的功能签名,因此不可能有明确匹配其中之一的确切方法引用。您的方法参考…::&lt;Functional1&gt;hitIt 与您的想法不同。它将T 替换为Functional1,因此生成的功能签名是() -&gt; Functional1,它与&lt;T&gt; () -&gt; T 不匹配
  • @DidierL 回答不同的问题会适得其反。特别是当它以“据我所知,这应该可以编译。”开头时,对于读者来说,“this”并不指代的代码并不明显。问题,但是您描述为“让我们简化代码”的答案中的代码实际上是一个完全不同的场景。现在,读者必须一直向下滚动到您的评论才能发现“您的问题的原始代码......不应该编译
  • @DidierL 当我使用问题的原始代码时,我在 Eclipse 中得到 The type of hitIt() from the type ImplementingClass is Functional1, this is incompatible with the descriptor's return type: T,在 javac 中得到 incompatible types: bad return type in method reference … Functional1 cannot be converted to T
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-11-18
  • 2012-01-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多