Lambda表达式和方法重载疑问

时间:2021-06-11 00:07:42

OK, so method overloading is-a-bad-thing™. Now that this has been settled, let's assume I actually want to overload a method like this:

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

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

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

In Java 7, I could call them easily with non-ambiguous anonymous classes as arguments:

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

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

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

Now in Java 8, I'd like to call those methods with lambda expressions of course, and I can!

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

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

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

Since the compiler should be able to infer Integer, why don't I leave Integer away, then?

由于编译器应该能够推断出Integer,为什么我不把Integer带走呢?

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

// Function
run(i -> 1);

But this doesn't compile. The compiler (javac, jdk1.8.0_05) doesn't like that:

但这不编译。编译器(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

To me, intuitively, this doesn't make sense. There is absolutely no ambiguity between a lambda expression that yields a return value ("value-compatible") and a lambda expression that yields void ("void-compatible"), as set out in the JLS §15.27.

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

But of course, the JLS is deep and complex and we inherit 20 years of backwards compatibility history, and there are new things like:

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

Certain argument expressions that contain implicitly typed lambda expressions (§15.27.1) or inexact method references (§15.13.1) are ignored by the applicability tests, because their meaning cannot be determined until a target type is selected.

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

from JLS §15.12.2

来自JLS§15.12.2

The above limitation is probably related to the fact that JEP 101 wasn't implemented all the way, as can be seen here and here.

上述限制可能与JEP 101没有一直实现的事实有关,这可以在这里和这里看到。

Question:

Who can tell me exactly what parts of the JLS specifies this compile-time ambiguity (or is it a compiler bug)?

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

Bonus: Why were things decided this way?

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

Update:

With jdk1.8.0_40, the above compiles and works fine

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

3 个解决方案

#1


18  

I think you found this bug in the compiler: JDK-8029718 (or this similar one in Eclipse: 434642).

我认为您在编译器中发现了这个错误:JDK-8029718(或Eclipse中的类似问题:434642)。

Compare to JLS §15.12.2.1. Identify Potentially Applicable Methods:

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

...

  • A lambda expression (§15.27) is potentially compatible with a functional interface type (§9.8) if all of the following are true:

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

    • The arity of the target type's function type is the same as the arity of the lambda expression.

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

    • If the target type's function type has a void return, then the lambda body is either a statement expression (§14.8) or a void-compatible block (§15.27.2).

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

    • If the target type's function type has a (non-void) return type, then the lambda body is either an expression or a value-compatible block (§15.27.2).

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

Note the clear distinction between “void compatible blocks” and “value-compatible blocks”. While a block might be both in certain cases, the section §15.27.2. Lambda Body clearly states that an expression like () -> {} is a “void compatible block”, as it completes normally without returning a value. And it should be obvious that i -> {} is a “void compatible block” too.

注意“void compatible blocks”和“value-compatible blocks”之间的明显区别。虽然在某些情况下可能同时存在阻塞,但第15.27.2节。 Lambda Body明确指出类似于() - > {}的表达式是“void兼容块”,因为它通常完成而不返回值。显而易见的是,i - > {}也是一个“无效兼容块”。

And according to the section cited above, the combination of a lambda with a block that is not value-compatible and target type with a (non-void) return type is not a potential candidate for the method overload resolution. So your intuition is right, there should be no ambiguity here.

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

Examples for ambiguous blocks are

模糊块的示例是

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

as they don’t complete normally, but this is not the case in your question.

因为它们没有正常完成,但在你的问题中并非如此。

#2


3  

This bug has already been reported in the JDK Bug System: https://bugs.openjdk.java.net/browse/JDK-8029718. As you can check the bug has been fixed. This fix syncs javac with the spec in this aspect. Right now javac is correctly accepting the version with implicit lambdas. To get this update, you need to clone javac 8 repo.

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

What the fix does is to analyze the lambda body and determine if it's void or value compatible. To determine this you need to analyze all return statements. Let's remember that from the spec (15.27.2), already referenced above:

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

  • A block lambda body is void-compatible if every return statement in the block has the form return.
  • 如果块中的每个return语句都具有返回形式,则块lambda主体是void兼容的。
  • A block lambda body is value-compatible if it cannot complete normally (14.21) and every return statement in the block has the form return Expression.
  • 如果块lambda body无法正常完成(14.21)并且块中的每个return语句都具有返回Expression的形式,则它是值兼容的。

This means that by analyzing the returns in the lambda body you can know if the lambda body is void compatible but to determine if it's value compatible you also need to do a flow analysis on it to determine that it can complete normally (14.21).

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

This fix also introduces a new compiler error for cases when the body is neither void nor value compatible, for example if we compile this code:

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

the compiler will give this output:

编译器会给出这个输出:

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

I hope this helps.

我希望这有帮助。

#3


0  

Lets assume we have method and method call

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

void run(Function<Integer, Integer> f)

run(i->i)

What methods can we legally add?

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

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

Here the parameter arity is different, specifically the i-> part of i->i does not fit the parameters of apply(T,U) in BiFunction, or get() in Supplier. So here any possible ambiguities are defined by parameter arity, not types, and not the return.

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


What methods can't we add?

我们不能添加哪些方法?

void run(Function<Integer, String> f)

This gives a compiler error as run(..) and run(..) have the same erasure. So as the JVM can't support two functions with the same name and argument types, this can't be compiled. So the compiler never has to resolve ambiguities in this type of scenario as they are explicitly disallowed due the rules preexisting in the Java type system.

这给出了编译器错误,因为run(..)和run(..)具有相同的擦除。因为JVM不能支持具有相同名称和参数类型的两个函数,所以无法编译。因此,编译器永远不必解决此类场景中的歧义,因为Java类型系统中预先存在的规则明确禁止它们。

So that leaves us with other functional types with a parameter arity of 1.

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

void run(IntUnaryOperator f)

Here run(i->i) is valid for both Function and IntUnaryOperator, but this will refuse to compile due to reference to run is ambiguous as both functions match this lambda. Indeed they do, and an error here is to be expected.

这里run(i-> i)对Function和IntUnaryOperator都有效,但是由于两个函数都匹配这个lambda,因为引用run是不明确的,所以这将拒绝编译。确实他们这样做了,这里有一个错误。

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

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

Here this fails to compile, again due to ambiguities. Without knowing the type of i in this lambda it is impossible to know the type of i.thing(). We therefore accept that this is ambiguous and rightly fails to compile.

由于含糊不清,这里无法编译。如果不知道这个lambda中i的类型,就不可能知道i.thing()的类型。因此,我们接受这是模棱两可的,并且无法编译。


In your example:

在你的例子中:

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

Here we know that both of functional types have a single Integer parameter, so we know that the i in i-> must be an Integer. So we know that it must be run(Function) that is called. But the compiler doesn't try to do this. This is the first time that the compiler does something that we don't expect.

在这里我们知道两种函数类型都有一个Integer参数,所以我们知道i->中的i必须是一个Integer。所以我们知道必须运行它(Function)。但编译器不会尝试这样做。这是编译器第一次执行我们不期望的操作。

Why does it not do this? I'd say because it is a very specific case, and inferring the type here requires mechanisms that we have not seen for any of the other above cases, because in the general case they are unable to correctly infer the type and choose the correct method.

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

#1


18  

I think you found this bug in the compiler: JDK-8029718 (or this similar one in Eclipse: 434642).

我认为您在编译器中发现了这个错误:JDK-8029718(或Eclipse中的类似问题:434642)。

Compare to JLS §15.12.2.1. Identify Potentially Applicable Methods:

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

...

  • A lambda expression (§15.27) is potentially compatible with a functional interface type (§9.8) if all of the following are true:

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

    • The arity of the target type's function type is the same as the arity of the lambda expression.

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

    • If the target type's function type has a void return, then the lambda body is either a statement expression (§14.8) or a void-compatible block (§15.27.2).

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

    • If the target type's function type has a (non-void) return type, then the lambda body is either an expression or a value-compatible block (§15.27.2).

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

Note the clear distinction between “void compatible blocks” and “value-compatible blocks”. While a block might be both in certain cases, the section §15.27.2. Lambda Body clearly states that an expression like () -> {} is a “void compatible block”, as it completes normally without returning a value. And it should be obvious that i -> {} is a “void compatible block” too.

注意“void compatible blocks”和“value-compatible blocks”之间的明显区别。虽然在某些情况下可能同时存在阻塞,但第15.27.2节。 Lambda Body明确指出类似于() - > {}的表达式是“void兼容块”,因为它通常完成而不返回值。显而易见的是,i - > {}也是一个“无效兼容块”。

And according to the section cited above, the combination of a lambda with a block that is not value-compatible and target type with a (non-void) return type is not a potential candidate for the method overload resolution. So your intuition is right, there should be no ambiguity here.

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

Examples for ambiguous blocks are

模糊块的示例是

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

as they don’t complete normally, but this is not the case in your question.

因为它们没有正常完成,但在你的问题中并非如此。

#2


3  

This bug has already been reported in the JDK Bug System: https://bugs.openjdk.java.net/browse/JDK-8029718. As you can check the bug has been fixed. This fix syncs javac with the spec in this aspect. Right now javac is correctly accepting the version with implicit lambdas. To get this update, you need to clone javac 8 repo.

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

What the fix does is to analyze the lambda body and determine if it's void or value compatible. To determine this you need to analyze all return statements. Let's remember that from the spec (15.27.2), already referenced above:

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

  • A block lambda body is void-compatible if every return statement in the block has the form return.
  • 如果块中的每个return语句都具有返回形式,则块lambda主体是void兼容的。
  • A block lambda body is value-compatible if it cannot complete normally (14.21) and every return statement in the block has the form return Expression.
  • 如果块lambda body无法正常完成(14.21)并且块中的每个return语句都具有返回Expression的形式,则它是值兼容的。

This means that by analyzing the returns in the lambda body you can know if the lambda body is void compatible but to determine if it's value compatible you also need to do a flow analysis on it to determine that it can complete normally (14.21).

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

This fix also introduces a new compiler error for cases when the body is neither void nor value compatible, for example if we compile this code:

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

the compiler will give this output:

编译器会给出这个输出:

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

I hope this helps.

我希望这有帮助。

#3


0  

Lets assume we have method and method call

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

void run(Function<Integer, Integer> f)

run(i->i)

What methods can we legally add?

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

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

Here the parameter arity is different, specifically the i-> part of i->i does not fit the parameters of apply(T,U) in BiFunction, or get() in Supplier. So here any possible ambiguities are defined by parameter arity, not types, and not the return.

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


What methods can't we add?

我们不能添加哪些方法?

void run(Function<Integer, String> f)

This gives a compiler error as run(..) and run(..) have the same erasure. So as the JVM can't support two functions with the same name and argument types, this can't be compiled. So the compiler never has to resolve ambiguities in this type of scenario as they are explicitly disallowed due the rules preexisting in the Java type system.

这给出了编译器错误,因为run(..)和run(..)具有相同的擦除。因为JVM不能支持具有相同名称和参数类型的两个函数,所以无法编译。因此,编译器永远不必解决此类场景中的歧义,因为Java类型系统中预先存在的规则明确禁止它们。

So that leaves us with other functional types with a parameter arity of 1.

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

void run(IntUnaryOperator f)

Here run(i->i) is valid for both Function and IntUnaryOperator, but this will refuse to compile due to reference to run is ambiguous as both functions match this lambda. Indeed they do, and an error here is to be expected.

这里run(i-> i)对Function和IntUnaryOperator都有效,但是由于两个函数都匹配这个lambda,因为引用run是不明确的,所以这将拒绝编译。确实他们这样做了,这里有一个错误。

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

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

Here this fails to compile, again due to ambiguities. Without knowing the type of i in this lambda it is impossible to know the type of i.thing(). We therefore accept that this is ambiguous and rightly fails to compile.

由于含糊不清,这里无法编译。如果不知道这个lambda中i的类型,就不可能知道i.thing()的类型。因此,我们接受这是模棱两可的,并且无法编译。


In your example:

在你的例子中:

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

Here we know that both of functional types have a single Integer parameter, so we know that the i in i-> must be an Integer. So we know that it must be run(Function) that is called. But the compiler doesn't try to do this. This is the first time that the compiler does something that we don't expect.

在这里我们知道两种函数类型都有一个Integer参数,所以我们知道i->中的i必须是一个Integer。所以我们知道必须运行它(Function)。但编译器不会尝试这样做。这是编译器第一次执行我们不期望的操作。

Why does it not do this? I'd say because it is a very specific case, and inferring the type here requires mechanisms that we have not seen for any of the other above cases, because in the general case they are unable to correctly infer the type and choose the correct method.

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