Lambda表达式和方法重载疑惑

49

好的,方法重载是件坏事™。既然这已经解决了,现在假设我实际上想像这样重载一个方法:

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

既然编译器可以推断出Integer,那我为什么不把Integer省略掉呢?

// 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

对我来说,直觉上这是没有意义的。正如JLS §15.27所述,返回值为“value-compatible”的lambda表达式和返回值为“void-compatible”的lambda表达式之间绝对没有歧义。
但当然,JLS非常深奥复杂,我们继承了20年的向后兼容历史,而且还有新的东西,比如:
特定的参数表达式中包含有隐式类型的lambda表达式(§15.27.1)或不精确的方法引用(§15.13.1),这些表达式在适用性测试中被忽略,因为它们的意义只能在选择目标类型之后确定。 来自JLS §15.12.2

上述限制可能与JEP 101没有完全实现有关,可以在这里这里看到。

问题:

谁能告诉我JLS的哪些部分规定了这种编译时的歧义(或者是编译器的bug)?

奖励:为什么会决定这样做?

更新:

使用jdk1.8.0_40,上述代码可以编译并正常工作。


2
@SyamS:i 是传递给 Consumer.accept()Function.apply() 的第一个(也是唯一的)参数。这本身可能会产生歧义。但是,考虑到一个 lambda 表达式评估为“值兼容”类型(Function),而另一个评估为“void 兼容”类型(Consumer),我直觉上认为没有歧义。 - Lukas Eder
3
抱歉如果这听起来很傻,但是函数重载通常只依赖于输入类型,不会检查返回类型。所以在这种情况下,accept和apply都接受一个整数类型的参数。所以对我来说它看起来是有歧义的。:) Lambda表达式是否需要返回类型进行推断? - Syam S
1
它在早期版本(例如beta 102及更早版本)中可以工作。 - Holger
1
@SyamS: i -> {}永远无法评估为Function,因为它是“void-compatible”的。i -> 1永远无法评估为Consumer,因为它是“value-compatible”的。在每次调用中,我认为只有一个重载方法是适用的。正如@jacobhyphenated所指出的那样,通过显式指定相同的函数参数类型(Integer i)可以解决歧义。 - Lukas Eder
1
@LukasEder 您是正确的。如果我尝试指定 run((Consumer<Integer>) (Integer i) -> {1});,它将无法编译。这让我相信这一定是编译器的错误,因为这两个lambda之间确实没有歧义。 - jacobhyphenated
显示剩余7条评论
3个回答

21
我想你发现了编译器中的JDK-8029718错误或者Eclipse中类似的434642错误)。
与之比较,参考JLS §15.12.2.1. 识别可能适用的方法
一个lambda表达式(§15.27)与一个函数接口类型(§9.8)可能兼容,如果以下所有条件都成立:
- 目标类型的函数类型的元数与lambda表达式的元数相同。 - 如果目标类型的函数类型具有void返回,则lambda体是语句表达式(§14.8)或void兼容块(§15.27.2)之一。 - 如果目标类型的函数类型具有(非void)返回类型,则lambda体是表达式或值兼容块(§15.27.2)之一。
请注意“void兼容块”和“值兼容块”之间的明显区别。虽然在某些情况下块可能都是,但§15.27.2。 Lambda Body部分明确指出像() -> {}这样的表达式是“void兼容块”,因为它完成正常而不返回值。而且,i -> {}也是一个“void兼容块”,这是显而易见的。

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

模糊块的示例:

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

由于它们不能正常完成,但这在您的问题中并不适用。

1
@Holger:我知道,它不应该适用。但请再次检查我的块引用部分:“某些参数表达式包含隐式类型的lambda表达式(§15.27.1)或不精确的方法引用(§15.13.1),这些表达式将被适用性测试忽略,因为在选择目标类型之前无法确定它们的含义。”也许,适用性部分对于隐式类型的lambda表达式只是被跳过了。 - Lukas Eder
4
这是正确答案。该漏洞已经修复 - https://bugs.openjdk.java.net/browse/JDK-8029718 - ZhongYu
1
@LukasEder lambda表达式在15.12.2.1中使用,以便我们只得到一个可能适用的方法(); 它在(15.12.2.2)中被忽略,而该方法被发现是适用的。由于这是唯一适用的方法,(15.12.2.5)不适用。 - ZhongYu
3
我认为Holger的回答非常完美。 - ZhongYu
1
只是为了闭合循环:在Eclipse中,这个bug是故意插入的,只是为了模仿javac的行为。在javac bug修复后不久,这个问题就被撤销了(请参见答案中的参考文献)。 - Stephan Herrmann
显示剩余13条评论

3
这个bug已经在JDK Bug System中报告过:https://bugs.openjdk.java.net/browse/JDK-8029718。你可以查看一下,这个bug已经被修复了。这个修复使得javac在这个方面上与规范同步。现在,javac可以正确地接受隐式lambda版本。要获取此更新,您需要克隆javac 8 repo
修复的作用是分析lambda体并确定它是否void或value兼容。为了确定这一点,您需要分析所有的返回语句。让我们记住,从规范(15.27.2)中引用:
  • 如果块lambda体中的每个返回语句都有形式return,则块lambda体是void-compatible。
  • 如果块lambda体不能正常完成(14.21),并且块中的每个返回语句都具有形式return Expression,则块lambda体是value-compatible。
这意味着通过分析lambda体中的返回值,您可以知道lambda体是否void compatible,但是要确定它是否value compatible,您还需要对其进行流分析以确定它是否可以正常完成(14.21)。
此修复还为既不是void也不是value compatible的情况引入了一个新的编译器错误,例如如果我们编译以下代码:
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

我希望这能有所帮助。

0
假设我们有一个方法和方法调用。
void run(Function<Integer, Integer> f)

run(i->i)

我们可以合法添加哪些方法?

void run(BiFunction<Integer, Integer, Integer> f)
void run(Supplier<Integer> f)

这里的参数arity不同,具体来说i->部分与BiFunction中的apply(T,U)Supplier中的get()的参数不匹配。因此,在这里,任何可能的歧义都是由参数arity而不是类型或返回值定义的。


我们不能添加哪些方法?

void run(Function<Integer, String> f)

这会导致编译器错误,因为run(..)和run(..)具有相同的擦除。因此,由于JVM无法支持具有相同名称和参数类型的两个函数,因此无法编译此代码。因此,编译器永远不必解决此类情况中的歧义,因为它们明确禁止由Java类型系统中预先存在的规则。

因此,我们只能使用具有参数数量为1的其他功能类型。

void run(IntUnaryOperator f)

这里的run(i->i)对于FunctionIntUnaryOperator都是有效的,但由于这两个函数都匹配此lambda,将无法编译,导致reference to run is ambiguous。事实上它们确实都匹配,因此出现错误是可以预料的。

interface X { void thing();}
interface Y { String thing();}

void run(Function<Y,String> f)
void run(Consumer<X> f)
run(i->i.thing())

这里编译失败了,原因同样是存在歧义。由于不知道lambda中i的类型,因此无法确定i.thing()的类型。因此,我们认为这是有歧义的,并且编译失败是正确的。


在你的例子中:

void run(Consumer<Integer> f)
void run(Function<Integer,Integer> f)
run(i->i)

在这里,我们知道两种函数类型都有一个单一的Integer参数,因此我们知道i->中的i必须是一个Integer。所以我们知道应该调用run(Function)。但编译器并没有尝试这样做。这是编译器第一次做出我们不期望的事情。

为什么它不这样做?我认为这是一个非常特殊的情况,推断类型需要机制,而我们在其他情况下没有看到过这些机制,因为在一般情况下,它们无法正确地推断类型并选择正确的方法。


2
关于 run(i->1) 是否应该编译,一直存在着激烈的争论。由于 i 显然是整数,因此这里没有任何歧义或困难。不幸的是,他们决定暂时不支持它,但为将来考虑留下了余地(如果有足够多的人需要此功能)。 - ZhongYu
@zhong.j.yu:我怀疑这场辩论也是你在这里引用的那个(http://mail.openjdk.java.net/pipermail/lambda-dev/2013-November/011394.html)? - Lukas Eder
1
@LukasEder,这是一场漫长而混乱的讨论,起始于http://mail.openjdk.java.net/pipermail/lambda-spec-observers/2013-August/000376.html - 我不建议你去阅读:) 没有人知道彼此在谈论什么。 - ZhongYu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接