Java 8 Consumer/Function Lambda Ambiguity

2022-09-01 13:22:47

我有一个重载方法,它分别采用 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);
    }
  }
}

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

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

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

答案 1

这行绝对是模棱两可的:

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)。

在你的例子中,你有一个语句表达式,因为任何方法调用都可以用作语句表达式,即使该方法是非vault的。例如,您可以简单地编写以下内容:Consumer

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

它没有意义,但编译得很完美。你的方法可能有副作用,编译器不知道它。例如,它是否总是返回,没有人关心它的返回值。List.addtrue

当然,这个lambda也有资格,因为它是一个表达式。因此,这是模棱两可的。如果你有一个表达式,但不是语句表达式,那么调用将被映射到没有任何问题:FunctionFunction

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

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

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

此块同时符合值兼容和 void 兼容的条件,因此您再次具有歧义。另一个模棱两可的块示例是无休止循环:

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

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

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

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

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

一般来说,我建议你避免重载你的方法,当你的重载具有相同数量的参数并且仅在接受的功能接口上有区别时。即使这些功能接口具有不同的 arity(例如,和 ),lambda 也不会有问题,但方法引用可能会有问题。在这种情况下,只需为方法选择不同的名称(例如,和 )。ConsumerBiConsumerprocessStuffconsumeStuff


答案 2