Java 8消费者/functionLambda歧义

我有一个重载方法,它分别接受一个Consumer和一个Function对象,并返回一个匹配相应的Consumer / Function的generics类型。 我认为这样会好,但是当我尝试使用lambda表达式调用任一方法时,我得到一个错误,指示对该方法的引用是不明确的。

基于我对JLS§15.12.2.1的阅读。 确定可能适用的方法:似乎编译器应该知道我的带有void块的lambda与Consumer方法匹配,而我的带有返回类型的lambda与Function方法匹配。

我把以下无法编译的示例代码放在一起:

import java.util.function.Consumer; import java.util.function.Function; public class AmbiguityBug { public static void main(String[] args) { doStuff(getPattern(x -> System.out.println(x))); doStuff(getPattern(x -> String.valueOf(x))); } static Pattern getPattern(Function function) { return new Pattern(function); } static ConsumablePattern getPattern(Consumer consumer) { return new ConsumablePattern(consumer); } static void doStuff(Pattern pattern) { String result = pattern.apply("Hello World"); System.out.println(result); } static void doStuff(ConsumablePattern consumablePattern) { consumablePattern.consume("Hello World"); } public static class Pattern { private final Function function; public Pattern(Function function) { this.function = function; } public R apply(T value) { return function.apply(value); } } public static class ConsumablePattern { private final Consumer consumer; public ConsumablePattern(Consumer consumer) { this.consumer = consumer; } public void consume(T value) { consumer.accept(value); } } } 

我还发现了一个类似的 stackoverflowpost,结果certificate是编译器错误。 我的情况非常相似,虽然有点复杂。 对我来说,这仍然看起来像一个bug,但我想确保我不会误解lambdas的语言规范。 我正在使用Java 8u45,它应该具有所有最新的修复程序。

如果我将我的方法调用更改为包裹在块中,则所有内容似乎都会编译,但这会增加额外的冗长度,并且许多自动格式化程序会将其重新格式化为多行。

 doStuff(getPattern(x -> { System.out.println(x); })); doStuff(getPattern(x -> { return String.valueOf(x); })); 

这条线肯定含糊不清:

 doStuff(getPattern(x -> String.valueOf(x))); 

从链接的JLS章节重读:

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

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

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

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

对于Consumer的情况,您有一个语句表达式,因为任何方法调用都可以用作语句表达式,即使该方法是非void也是如此。 例如,您可以简单地写这个:

 public void test(Object x) { String.valueOf(x); } 

它毫无意义,但编译完美。 您的方法可能有副作用,编译器不知道它。 例如, List.add总是返回true而没人关心它的返回值。

当然这个lambda也有资格获得Function因为它是一个表达式。 这就是它的暧昧。 如果你有一个表达式而不是语句表达式 ,那么调用将被映射到Function而没有任何问题:

 doStuff(getPattern(x -> x == null ? "" : String.valueOf(x))); 

当你将它改为{ return String.valueOf(x); } 您创建了一个与值兼容的块 ,因此它与Function匹配,但它不符合void兼容块 。 但是,您也可能遇到块问题:

 doStuff(getPattern(x -> {throw new UnsupportedOperationException();})); 

此块既可以兼容值,也可以兼容void,因此您会再次出现歧义。 另一个ambigue块示例是无限循环:

 doStuff(getPattern(x -> {while(true) System.out.println(x);})); 

至于System.out.println(x)情况,它有点棘手。 它肯定有资格作为语句表达式 ,因此可以与Consumer匹配,但似乎它与表达式匹配,并且spec表示方法调用是一个表达式。 然而, 如15.12.3所说,这是有限使用的表达:

如果编译时声明为void,则方法调用必须是顶级表达式(即表达式语句中的表达式或for语句的ForInit或ForUpdate部分),否则会发生编译时错误。 这样的方法调用不会产生任何值,因此只能在不需要值的情况下使用。

所以编译器完全遵循规范。 首先,它确定你的lambda主体既被限定为表达式(即使它的返回类型为void:15.12.2.1对于这种情况也不例外)和语句表达式,所以它也被认为是歧义。

因此,对我来说,两个语句都按照规范进行编译。 ECJ编译器在此代码上生成相同的错误消息。

一般情况下,我建议您在重载具有相同数量的参数时避免重载方法,并且仅在接受的function接口中有所不同。 即使这些function接口具有不同的arity(例如, ConsumerBiConsumer ):lambda也没有问题,但是方法引用可能有问题。 在这种情况下,只需为方法选择不同的名称(例如, processStuffconsumeStuff )。