【问题标题】:Lambda expression and method overloading doubtsLambda表达式和方法重载的疑惑
【发布时间】:2014-06-19 07:09:23
【问题描述】:

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

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 表达式和产生 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,上面的编译和工作正常

【问题讨论】:

  • @SyamS:iConsumer.accept()Function.apply() 的第一个(也是唯一一个)参数。这本身可能是模棱两可的。但鉴于一个 lambda 评估为“值兼容”类型(Function),另一个评估为“无效兼容”类型(Consumer),我直觉上认为没有歧义
  • 对不起,如果这听起来很傻,但函数重载通常只取决于输入类型。它不检查返回类型。所以在这种情况下,accept 和 apply 都接受一个整数类型的参数。所以对我来说它看起来模棱两可。 :) lambda 是否寻找返回类型进行推理?
  • 它确实适用于早期版本(例如beta 102 和更早版本)。
  • @SyamS 这似乎有点模棱两可,但编译器可以确定 run((Integer i) -&gt; {}) 是消费者。因此,尽管它可能是 Function 或 Consumer,但 Consumer 是最佳匹配,编译器会使用它。问题是,为什么编译器只在您指定(Integer i) 而不仅仅是i 时才这样做。
  • @SyamS: i -&gt; {} 永远不能评估为Function,因为它是“无效兼容的”。 i -&gt; 1 永远无法评估为 Consumer,因为它是“值兼容的”。在我看来,对于每个调用,只有一个重载方法甚至是适用。正如@jacobhyphenated 还指出的那样,可以通过显式指定相同函数参数类型(Integer i)来解决歧义。

标签: java lambda java-8 overloading jls


【解决方案1】:

我想你找到了this bug in the compiler: JDK-8029718 (or this similar one in Eclipse: 434642)。

对比JLS §15.12.2.1. Identify Potentially Applicable Methods

  • 如果满足以下所有条件,则 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 部分明确指出像() -&gt; {} 这样的表达式是“void 兼容块”,因为它正常完成而不返回值。很明显i -&gt; {} 也是一个“void 兼容块”。

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

模棱两可的块的例子是

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

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

【讨论】:

  • 对于 lambda i-&gt;i.thing(),如果不知道 i 的类型,您将无法判断这是 void 还是 value。这可能是问题而不是编译器错误。
  • @Holger:我知道,它不应该适用。但是再次检查我的块引用部分:“某些包含隐式类型的 lambda 表达式(第 15.27.1 节)或不精确的方法引用(第 15.13.1 节)的参数表达式被适用性测试忽略,因为直到它们的含义才能确定选择了一个目标类型。” 也许,对于隐式类型化的 lambda 表达式,适用性部分会被简单地跳过
  • 这是正确答案。该错误已修复 - bugs.openjdk.java.net/browse/JDK-8029718
  • @LukasEder lambda 表达式在 15.12.2.1 中使用,所以我们只有一个 可能适用的方法 ();在(15.12.2.2)中被忽略,发现该方法适用。由于这是唯一适用的方法,(15.12.2.5)不适用。
  • @LukasEder 我认为 Holger 的回答很完美。
【解决方案2】:

此错误已在 JDK 错误系统中报告:https://bugs.openjdk.java.net/browse/JDK-8029718。如您所见,该错误已得到修复。此修复将 javac 与这方面的规范同步。现在 javac 正在正确地接受带有隐式 lambda 的版本。要获取此更新,您需要克隆 javac 8 repo

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

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

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

此修复还针对主体既不为 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

我希望这会有所帮助。

【讨论】:

    【解决方案3】:

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

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

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

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

    这里的参数arity是不同的,特别是i-&gt;i中的i-&gt;部分不适合BiFunctionapply(T,U)的参数,或者Supplierget()的参数。所以这里任何可能的歧义都是由参数 arity 定义的,而不是类型,而不是返回。


    我们不能添加哪些方法?

    void run(Function<Integer, String> f)
    

    这会导致编译器错误为run(..) and run(..) have the same erasure。因此,由于 JVM 无法支持具有相同名称和参数类型的两个函数,因此无法编译。因此,编译器永远不必解决这种场景中的歧义,因为 Java 类型系统中预先存在的规则明确禁止了歧义。

    这样我们就剩下参数数量为 1 的其他函数类型了。

    void run(IntUnaryOperator f)
    

    这里run(i-&gt;i)FunctionIntUnaryOperator 都有效,但是由于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())
    

    这里编译失败,同样是由于模棱两可。如果不知道这个 lambda 中 i 的类型,就不可能知道 i.thing() 的类型。因此,我们承认这是模棱两可的并且正确地无法编译。


    在你的例子中:

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

    这里我们知道两个函数类型都有一个Integer 参数,所以我们知道i-&gt; 中的i 必须是Integer。所以我们知道它必须是run(Function) 被调用。但是编译器不会尝试这样做。这是编译器第一次做我们意想不到的事情。

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

    【讨论】:

    • 关于run(i-&gt;1) 是否应该编译的争论一直很激烈。这里没有歧义或困难,因为i 显然是整数。不幸的是,他们决定此时不支持它,而是为将来考虑(如果有足够多的人需要此功能)敞开大门
    • @zhong.j.yu:我怀疑这个辩论也是the one you've cited here
    • @LukasEder 从mail.openjdk.java.net/pipermail/lambda-spec-observers/… 开始,这是一个漫长而令人困惑的讨论——我不建议你阅读它:) 没有人知道彼此在说什么。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-12-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多