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

时间:2018-01-19 20:29:25

标签: java generics lambda

我注意到使用泛型和lambda重载方法的奇怪行为。这个类很好用:

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

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

  public void test() {
    test("test");
    test(() -> "test");
  }

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

  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
  }

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

编辑:这是使用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)

2 个答案:

答案 0 :(得分:2)

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

  

适用性测试会忽略某些包含隐式类型的lambda表达式(第15.27.1节)或不精确的方法引用(第15.13.1节)的参数表达式,因为在选择目标类型之前无法确定它们的含义。

当Java编译器遇到诸如test(() -> "test")的方法调用表达式时,它必须搜索可以调度此方法调用的可访问(可见)和适用(即具有匹配签名)方法。在您的第一个示例中,<T> void test(T)<T> void test(Supplier<T>)都可以访问并适用于w.r.t. test(() -> "test")方法调用。在这种情况下,当存在多个匹配方法时,编译器会尝试确定最具体的方法。现在,虽然这是对通用方法的确定(如 JLS 15.12.2.5JLS 18.5.4)非常复杂,我们可以使用15.12.2.5开头的直觉:

  

非正式的直觉是,如果第一个方法处理的任何调用都可以传递给另一个没有编译时错误的调用,那么一个方法比另一个方法更具体。

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

现在,令人惊讶的是,在您的第二个示例中,<T> void test(Class<T>, Supplier<T>)<T> void test(Class<T>, T)都被认为适用于方法调用test(String.class, () -> "test") ,即使它很明确对我们来说,后者不应该。问题是,如上所述,编译器在存在隐式类型的lambdas时非常保守。特别参见JLS 18.5.1

  

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

     

...

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

如果k≠n,或者存在i(1≤i≤n)使得e_i与适用性相关(§15.12.2.2)(...)否则,C包括,对于所有i(1≤i≤k),其中e_i与适用性相关,

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

如果k≠n,则该方法不适用,无需进行推理。

     

否则,C包括,对于所有i(1≤i≤k),其中e_i与适用性相关,

JLS 15.12.2.2

  

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

     
      
  • 隐式类型的lambda表达式(第15.27.1节)。
  •   
     

...

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

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

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

答案 1 :(得分:0)

这几天来,这个问题一直困扰着我。

我不会详细介绍JLS的细节,而是更多的逻辑解释。

如果我打电话怎么办?

test(Callable.class, () -> new Callable() {

    @Override
    public Object call() throws Exception {
        return null;
    }

});

编译器可以选择哪种方法

public <T> void test(Class<T> c, T t) 
same as below >> 
public <Callable> void test(Class<Callable> c, Callable<Callable> t)

Callable是一个@FunctionalInterface,所以看起来非常有效

public <T> void test(Class<T> c, Supplier<T> t)
same as below >>
public <Callable> void test(Class<Callable> c, Supplier<Callable> t)

供应商是一个@FunctionalInterface,所以它似乎也完全有效

当提供一个作为FunctionalInterface的类时,这两个调用是有效的,并且这两个调用都不比另一个调用更具体,这导致了一个模糊的方法消息。

那么只有一个参数的函数怎么样:

如果我打电话怎么办?

test(() -> new Callable() {

    @Override
    public Object call() throws Exception {
        return null;
    }

})

编译器可以选择哪种方法

public <T> void test(T t)

lambda表达式必须映射到@FunctionalInterface,但在这种情况下,我们没有提供任何显式类型来映射,因此这个调用无效

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

供应商是@FunctionalInterface,因此lambda表达式可以映射到@FunctionalInterface(供应商),然后调用有效

这两种方法中只有一种适用

在编译期间,所有泛型类型都被擦除,因此使用Object并且Object可以是FunctionalInterface

我希望有更多符合JLS标准的人可以验证或纠正我的解释,这会很棒。