Java 8 Consumer / Function Lambda Ambiguity

时间:2015-06-01 23:19:28

标签: java lambda java-8 overloading jls

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

基于我对JLS §15.12.2.1. Identify Potentially Applicable Methods:的阅读,似乎编译器应该知道我的带有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);
    }
  }
}

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

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

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

1 个答案:

答案 0 :(得分:19)

这一行肯定含糊不清:

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的情况下,您有一个statement expression,因为任何方法调用都可以用作语句表达式,即使该方法是非空的。例如,您可以简单地写下:

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); }时,您创建了value-compatible block,因此它与Function匹配,但它不符合 void-compatible块。但是,您也可能遇到块问题:

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

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

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

关于System.out.println(x)案例,这有点棘手。它肯定符合语句表达式,因此可以与Consumer匹配,但似乎它与表达式匹配,并且规范说method invocation是表达式。然而,这是有限使用的表达like 15.12.3说:

  

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

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

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

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