Java 8使用者/函数Lambda歧义


问题内容

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

基于我对JLS§15.12.2.1的阅读确定潜在的适用方法:似乎编译器应该知道我的带空块的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);
    }
  }
}

我还发现了一个类似的
stackoverflow帖子,原来是编译器错误。我的案子非常相似,尽管有点复杂。在我看来,这仍然像是个错误,但我想确保自己不会误解lambda的语言规范。我正在使用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节)兼容:

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

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

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

就您而言,Consumer您有一个语句表达式,因为即使方法是非无效的,任何方法调用都可以用作语句表达式。例如,您可以简单地编写以下代码:

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();}));

此块同时具有值​​兼容和空值兼容的资格,因此再次有歧义。另一个歧义块示例是一个无限循环:

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

至于System.out.println(x)情况,这有点棘手。它肯定符合 语句表达式的要求
,因此可以与匹配Consumer,但似乎它与表达式匹配,并且spec表示方法调用是表达式。但是,这是一种有限使用的表达方式,例如15.12.3说:

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

因此,编译器完全遵循规范。首先,它确定您的lambda主体符合表达式(即使返回类型为void:15.12.2.1在这种情况下也不例外)和语句表达式的资格,因此也被视为歧义。

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

通常,我建议您避免重载时具有相同数量的参数并且仅在可接受的功能接口上有所不同的方法重载。即使这些功能接口具有不同的通用性(例如ConsumerBiConsumer),您也不会遇到lambda的问题,但可能会遇到方法引用的问题。在这种情况下,只需为您的方法选择其他名称(例如processStuffconsumeStuff)。