使用Lambda表达式和泛型时,引用方法不明确。

23

我在以下代码中遇到了错误,但我认为这里不应该有错误... 使用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) {

    }
}

错误信息如下:

Error:(17, 9) java: reference to consumerIntFunctionTest is ambiguous both method consumerIntFunctionTest(java.util.function.Consumer,java.util.function.IntFunction) in net.tuis.ubench.Ambiguous and method consumerIntFunctionTest(java.util.function.Function,java.util.function.IntFunction) in net.tuis.ubench.Ambiguous match

错误发生在以下代码行:
consumerIntFunctionTest(Arrays::sort, int[]::new);

我认为不应该出现错误,因为所有的Arrays::sort引用都是void类型的,没有任何一个返回值。正如你所观察到的,当我明确扩展Consumer<T> lambda时,它确实起作用。
这真的是javac的一个bug吗?或者JLS规定lambda在这种情况下不能自动扩展?如果是后者,我仍然认为这很奇怪,因为带有第一个参数Function<T, ?>consumerIntFunctionTest不应该匹配。

2
这个应该定义在JLS的15.27.3章节中。(还没有详细查看过)。 - Jesper
3
为什么您认为 Function<T, ?> 不匹配?? 同样可以是 Void,因此它是匹配的。 - tomse
4
我认为这一定是某种错误:当注释掉“Consumer”方法时,它抱怨不能用给定的lambda调用“Function”方法,因此它无论如何都不可能是模棱两可的。有趣的是:当将参数声明为“(int[] data)”(从而使它成为一个明确类型的lambda)时,它会正确地将其解析为“Consumer”版本。在函数体中另外插入“return null;”时,它会解析为“Function”版本。所以很明显它遇到了在JLS中定义的隐式类型void兼容的lambda表达式问题。 - Marco13
2
我遇到了相同的错误,但由于tomse表示该代码在1.8.0_25下编译,这可能是1.8.0_40特定的问题。也许尝试在1.8.0_25下运行以查看是否可以编译该代码? - skomisa
4
鉴于您认为存在javac错误的次数与实际存在错误的次数相比,我认为首先在Stackoverflow上写一个问题,然后再编写错误报告,这样更合适。值得一提的是,我几个小时前已经提交了报告,但我仍在等待接受与否的答复。 - skiwi
显示剩余7条评论
2个回答

13
在你的第一个例子中。
consumerIntFunctionTest(data -> {
        Arrays.sort(data);
    }, int[]::new);

Lambda表达式具有void兼容块,可以通过表达式的结构来识别,而无需解析实际类型。
相比之下,在这个例子中:
consumerIntFunctionTest(Arrays::sort, int[]::new);

该方法引用必须被解析以确定它是否符合无返回值函数(Consumer)或有返回值函数(Function)的要求。简化的lambda表达式同样适用这一规则。
consumerIntFunctionTest(data -> Arrays.sort(data), int[]::new);

这可能是void-compatible或value-compatible的,具体取决于解析的目标方法。

问题在于,解析方法需要确定所需签名的知识,该签名应通过目标类型确定,但在了解泛型方法的类型参数之前,目标类型是未知的。虽然理论上两者可以同时确定,但规范中(尽管仍然非常复杂),方法重载分辨率首先进行,类型推断最后应用(参见JLS §15.12.2)。因此,类型推断可以提供的信息不能用于解决重载分辨率的问题。

但请注意,15.12.2.1.确定可能适用的方法中描述的第一步包含:

根据以下规则,表达式可以“潜在地兼容”于目标类型:
- 如果所有以下条件都为真,则 lambda 表达式(§15.27)与函数接口类型(§9.8)潜在地兼容: - 目标类型的函数类型的元数与 lambda 表达式的元数相同。 - 如果目标类型的函数类型具有 void 返回,则 lambda 主体是语句表达式(§14.8)或 void 兼容块(§15.27.2)之一。 - 如果目标类型的函数类型具有(非 void)返回类型,则 lambda 主体是表达式或值兼容块(§15.27.2)之一。 - 如果方法引用表达式(§15.13)的函数接口类型具有 n 个元数,并且存在至少一个对于该方法引用表达式具有 n 个元数的潜在适用方法(§15.13.1),则方法引用表达式潜在地与函数接口类型兼容,其中以下情况之一成立: - 方法引用表达式采用 ReferenceType :: [TypeArguments] Identifier 的形式,且至少有一个可潜在应用的方法是 i)静态的并支持 n 个元数,或者 ii)不是静态的并支持 n-1 个元数。 - 方法引用表达式采用其他形式,且至少有一个可潜在应用的方法不是静态的。

注:“潜在适用性”的定义超出了基本的元数检查,还考虑了函数接口目标类型的存在和“形状”。在某些涉及类型参数推断的情况下,作为方法调用参数出现的 lambda 表达式直到重载解析之后才能正确地进行类型化。
在第一个示例中,一种方法是通过lambda的形状进行排序,而在使用方法引用或仅包含单个调用表达式的lambda表达式的情况下,两种可能适用的方法都会经过这个第一选择过程,并在类型推断帮助找到目标方法以确定它是void还是返回值方法之前产生“模棱两可”的错误。
请注意,就像使用x->{ foo(); }使lambda表达式明确地兼容void一样,您可以使用x->( foo() )来使lambda表达式明确地兼容值。
你可以进一步阅读这个回答,解释了联合类型推断和方法重载解析限制的这一决定是一个故意(但不容易)的决策。

这是否也可以解释为什么如果我在两个参数中删除<T>类型参数并将其替换为int [],我仍然会得到相同的错误?这似乎可以解决泛型问题,但仍然会出现错误。 - skiwi
2
正如链接答案中所阐述的(请参见“比较”示例),在重载决议期间不考虑lambda表达式/方法引用的返回类型。请注意,最近的编译器将在声明时就警告您存在重载方法的潜在歧义,而无需通过实际模棱两可的调用来找出。您知道,“目标类型未知”适用于编译器(遵循正式流程),而不是我们人类读者,并且不需要泛型,只需要严格排序解析步骤即可。 - Holger
x->( foo() ) 这个解决方法很好。但是你能解释一下这个上下文中圆括号的确切含义吗?这是 lambda 语法吗?还是 Java 语句的“正常”包装? - mkurz
2
@mkurz 这是对 表达式 的正常包装,因为 foo() 可以是一个表达式或语句,而 (foo()) 只能是一个表达式。例如,你可以写var result = (foo()); 但是你不能把(foo()); 写成一条语句。同样的,{ foo(); } 只能是一个语句,因为你可以在期望语句的地方写它,但你不能写成 var result = { foo(); } - Holger

0

使用方法引用,即使参数类型和返回类型完全不同,只要有另一个方法的元数(参数数量)匹配,仍然可以得到相同的结果。

例如:

static class Foo {

    Foo(Consumer<Runnable> runnableConsumer) {}

    Foo(BiFunction<Long, Long, Long> longAndLongToLong) {}
}

static class Bar {

    static void someMethod(Runnable runnable) {}

    static void someMethod(Integer num, String str) {}
}

无论如何,Bar.someMethod()都无法满足longAndLongToLong,但是下面的代码会发出相同的编译错误,涉及歧义:

new Foo(Bar::someMethod);

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

那二进制兼容性呢?

考虑一下如果Foo构造函数的longAndLongToLong版本不存在,但是在库更新中后来添加了它,或者Bar.someMethod()的两个参数版本不存在,但是后来添加了它:突然之前编译的代码可能会因此而出现错误。

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

幸运的是,二进制兼容性得到了保留。相关条款在13.4.23. Method and Constructor Overloading中。

添加重载现有方法或构造函数的新方法或构造函数不会破坏与先前存在的二进制文件的兼容性。每次调用要使用的签名是在编译这些现有二进制文件时确定的;....
虽然添加新的重载方法或构造函数可能会导致编译时错误,因为没有最具体的方法或构造函数(§15.12.2.5),但在执行程序时不会发生此类错误,因为在执行时不进行重载分辨率。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接