Thread.sleep 的方法参考不明确

Method reference is ambiguous for Thread.sleep

我遇到了一个奇怪的问题,其中对 Thread::sleep 的方法引用不明确,但具有相同签名的方法却不明确。

package test;    

public class Test
{
    public static void main(String[] args)
    {
        foo(Test::sleep, 1000L); //fine
        foo((FooVoid<Long>)Thread::sleep, 1000L); //fine
        foo(Thread::sleep, 1000L); //error
    }

    public static void sleep(long millis) throws InterruptedException
    {
        Thread.sleep(millis);
    }

    public static <P, R> void foo(Foo<P, R> function, P param) {}

    public static <P> void foo(FooVoid<P> function, P param) {}

    @FunctionalInterface
    public interface Foo<P, R> {
        R call(P param1) throws Exception;
    }

    @FunctionalInterface
    public interface FooVoid<P> {
        void call(P param1) throws Exception;
    }
}

我遇到了这 2 个错误:

Error:(9, 17) java: reference to foo is ambiguous
  both method <P,R>foo(test.Test.Foo<P,R>,P) in test.Test and method <P>foo(test.Test.FooVoid<P>,P) in test.Test match

Error:(9, 20) java: incompatible types: cannot infer type-variable(s) P,R
    (argument mismatch; bad return type in method reference
      void cannot be converted to R)

我看到的唯一区别是 Thread::sleepnative。它有什么改变吗?我不认为过载 Thread::sleep(long, int) 在这里发挥作用。为什么会这样?

编辑:使用 javac 1.8 版。0_111

您可以在自己的 class 中重现该问题,方法是将带有两个参数的方法 sleep 添加到 class 测试,如下所示:

public static void sleep(long millis) {
}

public static void sleep(long millis, int nanos) {
}

所以问题确实是方法sleep重载造成的

JLS 表示初始方法选择代码只查看功能接口的类型参数的数量 - 只有在第二阶段它才会查看功能接口内部方法的签名。

JLS 15.13:

It is not possible to specify a particular signature to be matched, for example, Arrays::sort(int[]). Instead, the functional interface provides argument types that are used as input to the overload resolution algorithm (§15.12.2).

(本节倒数第二段)

所以在 Thread::sleep 的情况下,void sleep(long) 可能匹配功能接口 FooVoid<P>,而重载 void sleep(long, int) 可能匹配功能接口 Foo<P, R>。这就是您收到 "reference to foo is ambiguous" 错误的原因。

当它试图进一步查看如何将 Foo<P, R> 与功能方法 R call(P param1) 匹配到方法 void sleep(long, int) 时,它发现这实际上是不可能的,并且你得到另一个编译错误:

test/Test.java:7: error: incompatible types: cannot infer type-variable(s) P,R
        foo(Thread::sleep, 1000L); // error
           ^
    (argument mismatch; bad return type in method reference
      void cannot be converted to R)

我个人认为这是某种递归,有点像这样:我们需要解析方法才能找到目标类型,但我们需要知道目标类型才能解析方法。这与特殊无效兼容性规则有关,但我承认我并不完全理解它。

当你有这样的东西时,事情会更有趣:

public static void cool(Predicate<String> predicate) {

}

public static void cool(Function<String, Integer> function) {

}

并尝试通过以下方式调用它:

cool(i -> "Test"); // this will fail compilation 

顺便说一句,如果你使你的 lambda 显式,这将起作用:

foo((Long t) -> Thread.sleep(t), 1000L);

问题是 Thread.sleepfoo 都超载了。所以存在循环依赖。

  • 为了找出使用哪个 sleep 方法,我们需要知道目标类型,即要调用哪个 foo 方法
  • 为了找出调用哪个 foo 方法,我们需要知道参数的函数签名,即我们选择了哪个 sleep 方法

虽然人类很清楚 reader 对于这种情况,只有一个 2×2 组合是有效的,编译器必须遵循适用于任意组合的正式规则,因此,语言设计者必须剪一个。

为了方法引用的有用性,对明确的引用有特殊处理,比如你的Test::sleep:

JLS §15.13.1

For some method reference expressions, there is only one possible compile-time declaration with only one possible invocation type (§15.12.2.6), regardless of the targeted function type. Such method reference expressions are said to be exact. A method reference expression that is not exact is said to be inexact.

请注意,此区别类似于隐式类型 lambda 表达式(arg -> expression)和显式类型 lambda 之间的区别表达式 ((Type arg) -> expression).

当您查看 JLS, §15.12.2.5., Choosing the Most Specific Method 时,您会看到方法引用的签名仅用于 exact 方法引用,就像选择正确的 foo,正确的sleep方法的决定还没有做出。

If e is an exact method reference expression (§15.13.1), then i) for all i (1 ≤ i ≤ k), Ui is the same as Vi, and ii) one of the following is true:

  • R₂ is void.
  • R₁ <: R₂.
  • R₁ is a primitive type, R₂ is a reference type, and the compile-time declaration for the method reference has a return type which is a primitive type.
  • R₁ is a reference type, R₂ is a primitive type, and the compile-time declaration for the method reference has a return type which is a reference type.

上述规则已在§15.12.2.5 中说明。对于非泛型方法,重定向到 §18.5.4 for generic methods (which applies here as your foo methods are generic), containing exactly the same rule,措辞略有不同。

由于在选择最具体的方法时没有考虑方法引用的签名,所以没有最具体的方法,foo的调用不明确。第二个编译器错误是继续处理源代码并可能报告更多错误的策略的结果,而不是在第一个错误时立即停止编译。 foo 的两次调用之一导致了“不兼容类型”错误,如果该调用正在发生,但实际上由于“调用不明确”错误已被排除。