使用泛型和lambdas重载方法时调用不明确的方法

Sco*_*son 7 java generics lambda

我注意到了使用泛型和lambdas重载方法的奇怪行为.这个类工作正常:

  public <T> void test(T t) { }

  public <T> void test(Supplier<T> t) { }

  public void test() {
    test("test");
    test(() -> "test");
  }
Run Code Online (Sandbox Code Playgroud)

没有模糊的方法调用.但是,将其更改为此会使第二个调用变得模糊:

  public <T> void test(Class<T> c, T t) { }

  public <T> void test(Class<T> c, Supplier<T> t) { }

  public void test() {
    test(String.class, "test");
    test(String.class, () -> "test"); // this line does not compile
  }
Run Code Online (Sandbox Code Playgroud)

怎么会这样?为什么添加另一个参数导致方法解析不明确?为什么它能说明第一个例子中供应商和对象之间的区别,而不是第二个例子?

编辑:这是使用1.8.0_121.这是完整的错误消息:

error: reference to test is ambiguous
    test(String.class, () -> "test");
    ^
  both method <T#1>test(Class<T#1>,T#1) in TestFoo and method <T#2>test(Class<T#2>,Supplier<T#2>) in TestFoo match
  where T#1,T#2 are type-variables:
    T#1 extends Object declared in method <T#1>test(Class<T#1>,T#1)
    T#2 extends Object declared in method <T#2>test(Class<T#2>,Supplier<T#2>)
/workspace/com/test/TestFoo.java:14: error: incompatible types: cannot infer type-variable(s) T
    test(String.class, () -> "test");
        ^
    (argument mismatch; String is not a functional interface)
  where T is a type-variable:
    T extends Object declared in method <T>test(Class<T>,T)
Run Code Online (Sandbox Code Playgroud)

kor*_*lar 4

如果我对 Java SE 8 JLS 第 15 章和第 18 章的理解是正确的,那么您问题的关键在于以下第15.12.2段的引用段的引用:

\n
\n

适用性测试将忽略某些包含隐式类型 lambda 表达式 (\xc2\xa715.27.1) 或不精确方法引用 (\xc2\xa715.13.1) 的参数表达式,因为在选择目标类型之前无法确定它们的含义。

\n
\n

当Java编译器遇到诸如 之类的方法调用表达式时test(() -> "test"),它必须搜索可将该方法调用分派到的可访问(可见)和适用(即具有匹配签名)的方法。在您的第一个示例中, 和<T> void test(T)<T> void test(Supplier<T>)可以访问并适用于test(() -> "test")方法调用。在这种情况下,当有多种匹配方法时,编译器会尝试确定最具体的一种。现在,虽然对泛型方法的确定(如JLS 15.12.2.5JLS 18.5.4中所述)相当复杂,但我们可以使用 15.12.2.5 开头的直觉:

\n
\n

非正式的直觉是,如果第一个方法处理的任何调用可以传递给另一个方法而不会出现编译时错误,则一个方法比另一个方法更具体。

\n
\n

由于对于任何有效的调用,我们都可以在 中<T> void test(Supplier<T>)找到类型参数的相应实例,因此前者比后者更具体。T<T> void test(T)

\n

现在,令人惊讶的部分是,在您的第二个示例中, 和 都<T> void test(Class<T>, Supplier<T>)<T> void test(Class<T>, T)认为适用于 method calltest(String.class, () -> "test"),尽管我们很清楚后者不应该适用。问题是,如上所述,在存在隐式类型 lambda 的情况下,编译器的行为非常保守。特别参见JLS 18.5.1

\n
\n

一组约束公式 C 的构造如下。

\n

...

\n
    \n
  • 通过严格调用来测试适用性:
  • \n
\n

如果 k \xe2\x89\xa0 n,或者如果存在 i (1 \xe2\x89\xa4 i \xe2\x89\xa4 n)使得 e_i 与适用性相关(\xc2\xa715.12.2.2) (...)否则,对于所有 i (1 \xe2\x89\xa4 i \xe2\x89\xa4 k),其中 e_i 与适用性相关,C 包括 \xe2\x80\xb9e_i \xe2\x86\x92 F_i \xce\xb8\xe2\x80\xba。

\n
    \n
  • 通过松散调用测试适用性:
  • \n
\n

如果k \xe2\x89\xa0 n,则该方法不适用,无需继续推理。

\n

否则,对于所有 i (1 \xe2\x89\xa4 i \xe2\x89\xa4 k),其中 e_i 与适用性相关,C 包括 \xe2\x80\xb9e_i \xe2\x86\x92 F_i \xce\xb8\ xe2\x80\xba。

\n
\n

JLS 15.12.2.2

\n
\n

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

\n
    \n
  • 隐式类型的 lambda 表达式 (\xc2\xa715.27.1)。
  • \n
\n

...

\n
\n

因此,作为参数传递的隐式类型 lambda 的约束不参与解决方法适用性检查上下文中的类型推断。

\n

现在,如果我们假设这两种方法都适用,那么问题(以及此示例与前一个示例之间的区别)在于,这些方法都不是更具体的。存在有效<T> void test(Class<T>, Supplier<T>)但无效的呼叫<T> void test(Class<T>, T)调用,反之亦然。

\n

这也解释了为什么要test(String.class, (Supplier<String>) () -> "test");编译,正如@Aomin\xc3\xa8 在上面的评论中提到的。(Supplier<String>) () -> "test")是一个显式类型的 lambda,因此被认为与适用性相关,编译器能够正确推断出这些方法中只有一种适用,并且不会发生冲突。

\n