Lambda 表达式和方法重载疑问

2022-08-31 22:14:44

好吧,所以方法重载是一件™坏事。现在已经解决了这个问题,让我们假设我实际上想要重载这样一个方法:

static void run(Consumer<Integer> consumer) {
    System.out.println("consumer");
}

static void run(Function<Integer, Integer> function) {
    System.out.println("function");
}

在Java 7中,我可以使用非模棱两可的匿名类作为参数来轻松调用它们:

run(new Consumer<Integer>() {
    public void accept(Integer integer) {}
});

run(new Function<Integer, Integer>() {
    public Integer apply(Integer o) { return 1; }
});

现在在Java 8中,我想用lambda表达式调用这些方法,当然我可以!

// Consumer
run((Integer i) -> {});

// Function
run((Integer i) -> 1);

既然编译器应该能够推断,那我为什么不离开呢?IntegerInteger

// Consumer
run(i -> {});

// Function
run(i -> 1);

但这不会编译。编译器(javac,jdk1.8.0_05)不喜欢这样:

Test.java:63: error: reference to run is ambiguous
        run(i -> {});
        ^
  both method run(Consumer<Integer>) in Test and 
       method run(Function<Integer,Integer>) in Test match

对我来说,直觉上,这是没有道理的。生成返回值(“值兼容”)的 lambda 表达式和生成返回值的 lambda 表达式(“void 兼容”)之间绝对没有歧义,如 JLS §15.27 中所述。void

但是,当然,JLS是深刻而复杂的,我们继承了20年的向后兼容性历史,并且有一些新的东西,例如:

适用性测试会忽略某些包含隐式类型的 lambda 表达式§15.27.1) 或不精确方法引用 (§15.13.1) 的参数表达式,因为在选择目标类型之前无法确定它们的含义。

根据 JLS §15.12.2

上述限制可能与JEP 101没有完全实现的事实有关,如这里这里所示。

问题:

谁能确切地告诉我JLS的哪些部分指定了这种编译时的歧义(或者它是编译器错误)?

奖励:为什么事情是这样决定的?

更新:

使用jdk1.8.0_40,上述编译并正常工作


答案 1

我想你在编译器中发现了这个错误:JDK-8029718或者Eclipse:434642中的这个类似的错误)。

JLS §15.12.2.1 比较。确定可能适用的方法

...

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

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

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

    • 如果目标类型的函数类型具有(非 void)返回类型,则 lambda 主体要么是表达式,要么是值兼容块 (§15.27.2)。

请注意“兼容块”和“值兼容块”之间的明确区别。虽然在某些情况下,块可能两者兼而有之,但§15.27.2节。Lambda Body 明确指出,类似这样的表达式是“兼容块”,因为它正常完成而不返回值。很明显,这也是一个“兼容块”。void() -> {}voidi -> {}void

根据上面引用的部分,lambda与不值兼容的块和具有(非)返回类型的目标类型的组合不是方法重载解决方案的潜在候选者。所以你的直觉是对的,这里不应该有歧义。void

模棱两可块的示例包括

() -> { throw new RuntimeException(); }
() -> { while (true); }

因为它们不能正常完成,但您的问题并非如此。


答案 2

此错误已在 JDK 错误系统中报告:https://bugs.openjdk.java.net/browse/JDK-8029718。正如您可以检查的错误是否已修复。此修复程序将 javac 与这方面的规范同步。现在,javac正在正确地接受带有隐式lambdas的版本。要获取此更新,您需要克隆 javac 8 存储库

修复程序的作用是分析 lambda 主体并确定它是否无效或值兼容。要确定这一点,您需要分析所有返回语句。让我们记住,从上面已经引用的规范(15.27.2)中:

  • 如果块中的每个 return 语句都具有形式 return,则块 lambda 主体是 void 兼容的。
  • 如果块 lambda 主体无法正常完成 (14.21),并且块中的每个 return 语句都具有返回表达式的形式,则该块 lambda 主体与值兼容。

这意味着,通过分析 lambda 主体中的返回值,您可以知道 lambda 主体是否与 void 兼容,但要确定它是否与值兼容,您还需要对其进行流分析,以确定它可以正常完成 (14.21)。

此修复程序还为正文既不与 void 也不与值兼容的情况引入了新的编译器错误,例如,如果我们编译以下代码:

class Test {
    interface I {
        String f(String x);
    }

    static void foo(I i) {}

    void m() {
        foo((x) -> {
            if (x == null) {
                return;
            } else {
                return x;
            }
        });
    }
}

编译器将给出以下输出:

Test.java:9: error: lambda body is neither value nor void compatible
    foo((x) -> {
        ^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error

我希望这有帮助。