Lambda表达式和方法重载疑问

时间:2014-05-02 14:43:15

标签: java lambda java-8 overloading jls

好的,所以方法重载是一个糟糕的事情。既然已经解决了这个问题,我们假设我实际上想要重载这样的方法:

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
对我来说,直观地说,这没有意义。在产生返回值的lambda表达式(“value-compatible”)和产生void(“void-compatible”)的lambda表达式之间绝对没有歧义,如JLS §15.27中所述。

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

  

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

     

from JLS §15.12.2

上述限制可能与JEP 101未完全实施的事实有关,可以看作herehere

问题:

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

奖金:为什么事情会这样决定?

更新

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

3 个答案:

答案 0 :(得分:19)

我认为您找到了this bug in the compiler: JDK-8029718or this similar one in Eclipse: 434642)。

JLS §15.12.2.1. Identify Potentially Applicable Methods比较:

  

...

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

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

    •   
    • 如果目标类型的函数类型具有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 :(得分:3)

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

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

  • 如果每个return语句都在一个块lambda体是void兼容的 该块的格式为return。
  • 如果无法完成,则块lambda body是值兼容的 通常(14.21)并且块中的每个return语句都有 表单返回表达式。

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

此修复程序还会在正文既不是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

我希望这会有所帮助。

答案 2 :(得分:0)

让我们假设我们有方法和方法调用

void run(Function<Integer, Integer> f)

run(i->i)

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

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

此处参数arity不同,具体而言,i->的{​​{1}}部分不符合i->i中的apply(T,U)BiFunctionget()的参数{1}}。所以这里任何可能的歧义都是由参数arity定义的,而不是类型,而不是返回。


我们可以添加哪些方法?

Supplier

这使编译器错误为void run(Function<Integer, String> f) 。因此,由于JVM不能支持具有相同名称和参数类型的两个函数,因此无法编译。因此,编译器永远不必解决此类场景中的歧义,因为Java类型系统中预先存在的规则明确禁止它们。

这样我们就可以使用参数arity为1的其他函数类型。

run(..) and run(..) have the same erasure

此处void run(IntUnaryOperator f) run(i->i)Function均有效,但由于IntUnaryOperator因两个函数都匹配此lambda而拒绝编译。事实上他们确实这样做了,这里有一个错误。

reference to run is ambiguous

由于含糊不清,这里无法编译。在不知道此lambda中interface X { void thing();} interface Y { String thing();} void run(Function<Y,String> f) void run(Consumer<X> f) run(i->i.thing()) 的类型的情况下,无法知道i的类型。因此,我们接受这是模棱两可的,并且无法编译。


在你的例子中:

i.thing()

我们知道两种功能类型都有一个void run(Consumer<Integer> f) void run(Function<Integer,Integer> f) run(i->i) 参数,因此我们知道Integer中的i必须是i->。所以我们知道它必须是Integer。但编译器并没有尝试这样做。这是编译器第一次做出我们不期望的事情。

为什么不这样做?我要说因为这是一个非常具体的案例,并且推断这里的类型需要我们在上述任何其他情况下都没有看到的机制,因为在一般情况下他们无法正确推断出类型并选择正确的方法。