Java 8 Consumer/Function Lambda Ambiguity

joh*_*cox 25 java lambda overloading jls java-8

我有一个重载方法,分别接受Consumer和Function对象,并返回一个匹配相应的Consumer/Function的泛型类型.我认为这样会好,但是当我尝试使用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<String, String> getPattern(Function<String, String> function) {
    return new Pattern<>(function);
  }

  static ConsumablePattern<String> getPattern(Consumer<String> consumer) {
    return new ConsumablePattern<>(consumer);
  }

  static void doStuff(Pattern<String, String> pattern) {
    String result = pattern.apply("Hello World");
    System.out.println(result);
  }

  static void doStuff(ConsumablePattern<String> consumablePattern) {
    consumablePattern.consume("Hello World");
  }

  public static class Pattern<T, R> {
    private final Function<T, R> function;

    public Pattern(Function<T, R> function) {
      this.function = function;
    }

    public R apply(T value) {
      return function.apply(value);
    }
  }

  public static class ConsumablePattern<T> {
    private final Consumer<T> consumer;

    public ConsumablePattern(Consumer<T> consumer) {
      this.consumer = consumer;
    }

    public void consume(T value) {
      consumer.accept(value);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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

doStuff(getPattern(x -> { System.out.println(x); }));
doStuff(getPattern(x -> { return String.valueOf(x); }));
Run Code Online (Sandbox Code Playgroud)

Tag*_*eev 19

这条线肯定含糊不清:

doStuff(getPattern(x -> String.valueOf(x)));
Run Code Online (Sandbox Code Playgroud)

从链接的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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

doStuff(getPattern(x -> x == null ? "" : String.valueOf(x)));
Run Code Online (Sandbox Code Playgroud)

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

doStuff(getPattern(x -> {throw new UnsupportedOperationException();}));
Run Code Online (Sandbox Code Playgroud)

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

doStuff(getPattern(x -> {while(true) System.out.println(x);}));
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

  • 为什么在 `Consumer&lt;A&gt;` 和 `Function&lt;A, B&gt;` 之间出现这种歧义,但在 `Runner` 和 `Supplier&lt;B&gt;` 之间却没有出现这种歧义? (2认同)
  • 即 `cf(i -&gt; Math.abs(i))` (其中 `cf` 是为 `Consumer&lt;A&gt;` 和 `Function&lt;A, B&gt;` 定义的)会产生歧义,但 `rs(() - &gt; Math.abs(0))` (其中 `rs` 是为 `Runner` 和 `Supplier&lt;B&gt;` 定义的)。 (2认同)