为什么这个 Java 8 方法引用会编译?

2022-09-04 02:38:28

我目前正在深入研究Lambda和方法参考等功能。稍微玩一下,我得出了下面的例子:Java 8

public class ConsumerTest {

  private static final String[] NAMES = {"Tony", "Bruce", "Steve", "Thor"};

   public static void main(String[] args) {
      Arrays.asList(NAMES).forEach(Objects::requireNonNull);
   }
}

我的问题是:

为什么 main 方法内部的行会编译?

如果我正确理解了这个问题,那么引用的方法的签名必须与功能接口的SAM签名相对应。在这种情况下,使用者需要以下签名:

void accept(T t);

但是,该方法返回而不是 void:requireNonNullT

public static <T> T requireNonNull(T obj)

答案 1

Java语言规范版本8在15.13.2中说:

如果 T 是函数接口类型 (§9.8),并且表达式与从 T 派生的地面目标类型的函数类型一致,则方法引用表达式在赋值上下文、调用上下文或强制转换上下文中与目标类型 T 兼容。

[..]

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

  • 函数类型标识与引用对应的单个编译时声明。
  • 以下情况之一为真:
    • 函数类型的结果是 void。
    • 函数类型的结果是 R,将捕获转换 (§5.1.10) 应用于所选编译时声明的调用类型 (§15.12.2.6) 的返回类型的结果为 R'(其中 R 是可用于推断 R' 的目标类型),并且 R 和 R' 都不是无效的,并且 R' 在赋值上下文中与 R 兼容。

(强调我的)

因此,函数类型的结果是 void 的事实足以让它匹配。

JLS 15.12.2.5 还特别提到了匹配方法时使用 void。对于 lambda 表达式,存在 void 兼容块 (15.27.2) 的概念,该块在 15.12.2.1 中被引用,但对于方法引用没有等效的定义。

我无法找到更具体的解释(但是JLS是一个很难破解的螺母,所以也许我错过了一些相关的部分),但我认为这与这样一个事实有关,即你也可以调用非void方法作为自己的语句(没有赋值等))。return

JLS 15.13.3 方法引用的运行时评估也说:

为了确定编译时结果,如果调用方法的结果是无效的,则方法调用表达式是表达式语句;如果调用方法的结果是非 void,则返回语句的表达式。

当方法引用的编译时声明是签名多态时,此确定的效果是:

  • 方法调用的参数类型是相应参数的类型。
  • 方法调用为 void 或具有 Object 的返回类型,具体取决于包含方法调用的调用方法是 void 还是具有返回类型。

因此,生成的方法调用将为空以匹配函数类型。


答案 2

除了它在@Mark Rotteveel的确切答案中所说的内容之外,它之所以编译,是因为在Java中,您可以忽略任何方法调用的结果,例如在下面的示例中:

Map<String, String> map = new HashMap<>();

map.put("1", "a");

map.put("1", "A"); // Who cares about the returned value "a"?

由于您没有返回消费者块中的任何内容,因此根据规范它是有效的。forEach()


推荐