使用lambdas和泛型时,对方法的引用是不明确的

时间:2015-03-28 22:47:56

标签: java lambda java-8

我收到以下代码的错误,我认为不应该存在...使用JDK 8u40编译此代码。

public class Ambiguous {
    public static void main(String[] args) {
        consumerIntFunctionTest(data -> {
            Arrays.sort(data);
        }, int[]::new);

        consumerIntFunctionTest(Arrays::sort, int[]::new);
    }

    private static <T> void consumerIntFunctionTest(final Consumer<T> consumer, final IntFunction<T> generator) {

    }

    private static <T> void consumerIntFunctionTest(final Function<T, ?> consumer, final IntFunction<T> generator) {

    }
}

错误如下:

  

错误:(17,9)java:对consumerIntFunctionTest的引用不明确     net.tuis.ubench.Ambiguous中的方法consumerIntFunctionTest(java.util.function.Consumer,java.util.function.IntFunction)和net中的方法consumerIntFunctionTest(java.util.function.Function,java.util.function.IntFunction) .tuis.ubench.Ambiguous match

错误发生在以下行:

consumerIntFunctionTest(Arrays::sort, int[]::new);

我认为应该没有错误,因为所有Arrays::sort引用都是void类型,并且它们都没有返回值。正如您所观察到的,当我明确展开Consumer<T> lambda时, 工作。

这真的是javac中的错误,还是JLS声明lambda在这种情况下无法自动展开?如果是后者,我仍然认为这很奇怪,因为consumerIntFunctionTest与第一个参数Function<T, ?>不匹配。

2 个答案:

答案 0 :(得分:9)

在你的第一个例子中

consumerIntFunctionTest(data -> {
        Arrays.sort(data);
    }, int[]::new);

lambda表达式具有void - 兼容块,可以通过表达式的结构来识别,而无需解析实际类型。

相反,在示例中

consumerIntFunctionTest(Arrays::sort, int[]::new);

必须解析方法引用,以确定它是否符合void函数(Consumer)或值返回函数(Function)。这同样适用于简化的lambda表达式

consumerIntFunctionTest(data -> Arrays.sort(data), int[]::new);

可以兼容void - 兼容或价值兼容,具体取决于已解决的目标方法。

问题在于解析方法需要有关所需签名的知识,这应该通过目标类型确定,但是在知道通用方法的类型参数之前,目标类型是未知的。虽然理论上两者都可以立即确定,但是在规范中已经简化了(仍然非常复杂)过程,该方法首先执行重载决策,最后应用类型推断(参见JLS §15.12.2)。因此,类型推断可以提供的信息不能用于解决重载决策。

但请注意15.12.2.1. Identify Potentially Applicable Methods中描述的第一步包含:

  

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

     
      
  • 如果满足以下所有条件,则lambda表达式(第15.27节)可能与功能接口类型(第9.8节)兼容:

         
        
    • 目标类型的函数类型的arity与lambda表达式的arity相同。

    •   
    • 如果目标类型的函数类型具有void返回,则lambda主体是语句表达式(§14.8)或void兼容块(§15.27.2)。

    •   
    • 如果目标类型的函数类型具有(非void)返回类型,则lambda主体是表达式或值兼容块(第15.27.2节)。

    •   
  •   
  • 方法引用表达式(第15.13节)可能与函数接口类型兼容,如果类型的函数类型arity为n,则至少存在一个可能适用于arity n的方法引用表达式的方法( §15.13.1),以下之一是真的:

         
        
    • 方法引用表达式的形式为ReferenceType :: [TypeArguments]标识符,并且至少一个可能适用的方法是i)static并支持arity n,或ii)not static并支持arity n-1。

    •   
    • 方法引用表达式有一些其他形式,至少有一个可能适用的方法不是静态的。

    •   
  •   
     

...

     

潜在适用性的定义超出了基本的arity检查,也考虑了功能接口目标类型的存在和“形状”。在某些涉及类型参数推断的情况下,在重载解析之前,无法正确键入作为方法调用参数出现的lambda表达式。

因此,在第一个示例中,其中一个方法按照lambda的形状进行排序,而在方法引用或由单个调用表达式组成的lambda表达式的情况下,两个可能适用的方法都会忍受第一个选择过程并产生“类型推断之前的错误“模糊”可以帮助找到目标方法,以确定它是void还是值返回方法。

请注意,与使用x->{ foo(); }明确void - 兼容的lambda表达式一样,您可以使用x->( foo() )使lambda表达式显式地与值兼容。


你疯狂地进一步阅读this answer解释说,组合类型推断和方法重载决策的这种限制是一个刻意(但不容易)的决定。

答案 1 :(得分:0)

使用方法引用,你可以有完全不同的参数类型,更不用说返回类型了,如果你有另一个方法的数量(参数数量)匹配,仍然会得到这个。

例如:

Bar.someMethod()

longAndLongToLong 不可能满足 new Foo(Bar::someMethod); ,但下面的代码会发出相同的关于歧义的编译错误:

longAndLongToLong

Holger 的回答很好地解释了这背后的 JLS 中的逻辑和相关条款。

二进制兼容性如何?

考虑是否 Foo 构造函数的 Bar.someMethod() 版本不存在但后来在库更新中添加,或者 #shutdown with button script dtoverlay=gpio-shutdown 的两个参数版本不存在但添加稍后:突然之前编译的代码可能因此而中断。

这是方法重载的一个不幸的副作用,甚至在 lambda 或方法引用出现之前,类似的问题就已经影响了普通的方法调用。

幸运的是,保留了二进制兼容性。相关子句在 13.4.23. Method and Constructor Overloading:

<块引用>

添加重载现有方法或构造函数的新方法或构造函数不会破坏与预先存在的二进制文件的兼容性。用于每个调用的签名是在编译这些现有二进制文件时确定的; ....

虽然添加新的重载方法或构造函数可能会在下次编译类或接口时导致编译时错误,因为没有最具体的方法或构造函数(第 15.12.2.5 节),但在以下情况下不会发生此类错误一个程序被执行,因为在执行时没有进行重载解析。