方法引用表达式为&Quot;Exact&Quot;的条件

人气:47 发布:2023-01-03 标签: java generics type-inference method-reference jls

问题描述

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

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

如果方法引用表达式具有ReferenceType::[TypeArguments]标识符格式,则ReferenceType不表示原始类型。 要搜索的类型正好有一个成员方法,其名称标识符可由出现方法引用表达式的类或接口访问。 此方法不是变量(§8.4.1)。 如果此方法是泛型的(§8.4.4),则方法引用表达式提供 类型参数。

考虑以下代码片段:

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的输入,我总结如下:

两个功能接口都有unctionType<T> () -> T 方法引用…::<Functional1>hitItT替换为Functional1,因此得到的函数签名为() -> Functional1,与<T> () -> T不匹配。

推荐答案

首先警告:iANAJL(用于JAVA的iANAL)

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

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≤i≤n),方法调用的i参数与方法的i参数的类型具有潜在的兼容性。

[…]

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

[…]

方法引用表达式(§15.13)可能与函数接口类型兼容,如果类型的函数类型为n,则该方法引用表达式至少存在一个可能适用的方法n(§15.13.1),并且下列条件之一成立: 方法引用表达式的形式为ReferenceType::[TypeArguments]IDENTIFIER,并且至少有一个可能适用的方法是i)static并且支持奇异性n,或者ii)NOTstatic并且支持奇异性n-1。 方法引用表达式具有其他形式,并且至少有一个可能适用的方法不是static

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

此时,我们只检查方法引用的一致性,因此这两个funct()方法都可能适用。

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

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

[…]

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

[…]

这是该列表中唯一可能匹配的要点,但是,正如问题所指出的,我们这里有一个精确的方法引用表达式。请注意,如果删除<Void>,这将使其成为不准确的方法引用,根据下一节:

,这两种方法都应该适用 设m是一个可能适用的方法(§15.12.2.1),具有多重性n和形参类型F1...Fn,并让e1,...,en作为方法调用的实际参数表达式。然后:

[…]

如果m不是泛型方法,则对于1≤i≤n,如果ei在严格调用上下文中与Fiei与适用性无关,则m通过严格调用适用。 但是,严格调用应该只适用第一个funct()方法声明。严格的调用上下文是定义的here,但基本上它们检查表达式的类型是否与参数的类型匹配。在这里,我们的参数类型,即方法引用,由15.13.2. Type of a Method Reference一节定义,其相关部分为:

如果T是函数接口类型(§9.8),并且该表达式与[…]的function type一致,则方法引用表达式在具有目标类型T的赋值上下文、调用上下文或强制转换上下文中兼容T.

[…]

如果满足以下两个条件,则方法引用表达式与函数类型一致:

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

下列情况之一为真:

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

这里,Functional1的R是IntegerFunctional2的R是String,而R'在这两种情况下都是Integer(因为ImplementingClass.hitIt()不需要捕获转换),所以很明显,方法引用与Functional2不一致,并且扩展起来不兼容。

因此,严格调用不应考虑funct(Functional2)是否适用,由于仅保留funct(Functional1),因此应选择它。

需要注意的是,在第一阶段中,必须选择两种方法,因为只有一个阶段可以应用,第二阶段只使用宽松上下文,而不是只允许装箱操作的严格,而第三阶段则包含varargs,这也不适用。

除非我们认为该方法引用与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(§4.10),则类型S比类型T更具体。

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

对于表达式e,如果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应使Functional1Integer <: Number开始更加具体。

此操作仍然失败,这似乎确认编译器不会将其视为确切的方法引用。

请注意,删除ImplementingClass.hitIt()中的<T>允许它独立于Functional2.hitIt()的返回类型进行编译。有趣的事实:您可以将<Void>留在调用位置,编译器会忽略它。

更奇怪的是:如果您离开<T>并在调用点添加了比所需更多的类型参数,编译器仍然会报告不明确的调用,而不是类型参数的数量(直到您消除了歧义)。根据上面的定义,这并不应该使方法引用不准确,但我认为it should be checked first。

结论

因为Eclipse编译器接受它,所以我倾向于认为这是一个JAVAC错误,但请注意,在规范方面,有时比JAVAC更宽松,并且已经报告并修复了一些类似的错误(JDK-8057895,JDK-8170842,…)。

14