【问题标题】:Java 8 Consumer/Function Lambda AmbiguityJava 8 消费者/函数 Lambda 歧义
【发布时间】:2015-08-15 14:02:12
【问题描述】:

我有一个重载方法,它分别接受消费者和函数对象,并返回与相应消费者/函数匹配的泛型类型。我认为这很好,但是当我尝试使用 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 帖子,结果证明这是一个编译器错误。我的情况非常相似,虽然有点复杂。对我来说,这仍然看起来像一个错误,但我想确保我没有误解 lambdas 的语言规范。我正在使用 Java 8u45,它应该具有所有最新修复。

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

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

【问题讨论】:

  • (应该)使用消费者的 getPattern 调用都缺少右括号(由于最小字符限制,我无法编辑它)。
  • 另一种解决方法是指定 x 的类型:doStuff(getPattern((String x) -&gt; System.out.println(x)));doStuff(getPattern((String x) -&gt; String.valueOf(x)));
  • @Misha:这对我来说实际上很奇怪。我没有合理的解释为什么添加 x 类型会改变事情。您是否期望此案例将映射到Function:Map&lt;String, String&gt; map = new HashMap&lt;&gt;();getPattern((String x) -&gt; map.remove(x));
  • @TagirValeev 见docs.oracle.com/javase/specs/jls/se8/html/…。如果 lambda 是显式类型的,则有一些特殊规则可以确定功能接口类型之间的“更具体”关系。根据这些规则,Function 似乎比 Consumer 更具体
  • 您可以使用x -&gt; (String.valueOf(x)) 调用基于Function 的方法,使用x-&gt;{String.valueOf(x);} 调用基于Consumer 的方法。但是考虑到这些方法做完全不同的事情并产生不同类型的结果,这会导致另一个方法的重载选择再次做完全不同的事情,为这些方法使用相同的名称是一种可怕的伪装。

标签: java lambda java-8 overloading jls


【解决方案1】:

这行肯定是模棱两可的:

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

从链接的 JLS 章节重读此内容:

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

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

  • 如果目标类型的函数类型返回 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 兼容块的条件。但是,您也可能遇到块问题:

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

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

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 编译器在此代码上产生相同的错误消息。

一般来说,当您的重载具有相同数量的参数并且仅在可接受的功能接口方面存在差异时,我建议您避免重载方法。即使这些功能接口有不同的数量(例如,ConsumerBiConsumer):你不会遇到 lambda 问题,但可能会遇到方法引用问题。在这种情况下,只需为您的方法选择不同的名称(例如,processStuffconsumeStuff)。

【讨论】:

  • 为什么Consumer&lt;A&gt;Function&lt;A, B&gt;之间会出现这种歧义,而RunnerSupplier&lt;B&gt;之间不会出现这种歧义?
  • cf(i -&gt; Math.abs(i))(其中cf 定义为Consumer&lt;A&gt;Function&lt;A, B&gt;)会产生歧义,但rs(() -&gt; Math.abs(0))(其中rs 定义为RunnerSupplier&lt;B&gt;)不会。跨度>
  • @Sasha 因为 “目标类型的函数类型的数量与 lambda 表达式的数量相同”。 在您的情况下,此类型是 A。返回类型 B 超出分析范围。
  • @Woland,Supplier&lt;B&gt;Runnable 的数量不一样吗?
  • @Sasha 供应商**T** get(),可运行**void** run()。因此,在@FunctionalInterface 的情况下,返回类型对重载有影响。
猜你喜欢
  • 2016-11-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-09-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多