简体   繁体   中英

Reference to method is ambiguous when using lambdas and generics

I am getting an error on the following code, which I believe should not be there... Using JDK 8u40 to compile this code.

public class Ambiguous {
    public static void main(String[] args) {
        consumerIntFunctionTest(data -> {
            Arrays.sort(data);
        }, int[]::new);

        consumerIntFunctionTest(Arrays::sort, int[]::new);
    }

    private static <T> void consumerIntFunctionTest(final Consumer<T> consumer, final IntFunction<T> generator) {

    }

    private static <T> void consumerIntFunctionTest(final Function<T, ?> consumer, final IntFunction<T> generator) {

    }
}

The error is the following:

Error:(17, 9) java: reference to consumerIntFunctionTest is ambiguous both method consumerIntFunctionTest(java.util.function.Consumer,java.util.function.IntFunction) in net.tuis.ubench.Ambiguous and method consumerIntFunctionTest(java.util.function.Function,java.util.function.IntFunction) in net.tuis.ubench.Ambiguous match

The error occurs on the following line:

consumerIntFunctionTest(Arrays::sort, int[]::new);

I believe there should be no error, as all Arrays::sort references are of type void , and none of them return a value. As you can observe, it does work when I explicitly expand the Consumer<T> lambda.

Is this really a bug in javac, or does the JLS state that the lambda cannot automatically be expanded in this case? If it is the latter, I would still think it is weird, as consumerIntFunctionTest with as first argument Function<T, ?> should not match.

In your first example

consumerIntFunctionTest(data -> {
        Arrays.sort(data);
    }, int[]::new);

the lambda expression has a void -compatible block which can be identified by the structure of the expression without the need to resolve the actual types.

In contrast, in the example

consumerIntFunctionTest(Arrays::sort, int[]::new);

the method reference has to be resolved to find out, whether it conforms to either, a void function ( Consumer ) or a value returning function ( Function ). The same applies to the simplified lambda expression

consumerIntFunctionTest(data -> Arrays.sort(data), int[]::new);

which could be both, void - compatible or value- compatible, depending on the resolved target method.

The problem is that resolving the method requires knowledge about the required signature, which ought to be determined via target typing, but the target type isn't known until the type parameters of the generic method are known. While in theory both could be determined at once, the (still being awfully complex) process has been simplified in the specification in that method overload resolution is performed first and type inference is applied last (see JLS §15.12.2 ). Hence, the information that type inference could provide cannot be used for solving overload resolution.

But note that the first step described in 15.12.2.1. Identify Potentially Applicable Methods contains:

An expression is potentially compatible with a target type according to the following rules:

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

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

    • 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).

    • 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).

  • A method reference expression (§15.13) is potentially compatible with a functional interface type if, where the type's function type arity is n, there exists at least one potentially applicable method for the method reference expression with arity n (§15.13.1), and one of the following is true:

    • The method reference expression has the form ReferenceType :: [TypeArguments] Identifier and at least one potentially applicable method is i) static and supports arity n, or ii) not static and supports arity n-1.

    • The method reference expression has some other form and at least one potentially applicable method is not static.

The definition of potential applicability goes beyond a basic arity check to also take into account the presence and "shape" of functional interface target types. In some cases involving type argument inference, a lambda expression appearing as a method invocation argument cannot be properly typed until after overload resolution .

So your in first example one of the methods is sorted out by the lambda's shape while in case of a method reference or a lambda expression consisting of a sole invocation expression, both potentially applicable methods endure this first selection process and yield an “ambiguous” error before type inference can kick in to aid finding a target method to determine if it's a void or value returning method.

Note that like using x->{ foo(); } x->{ foo(); } to make a lambda expression explicitly void -compatible, you can use x->( foo() ) to make a lambda expression explicitly value-compatible.


You mad further read this answer explaining that this limitation of combined type inference and method overload resolution was a deliberate (but not easy) decision.

With method references, you could have entirely different parameter types, let alone return types, and still get this if you have another method where the arity (number of arguments) matches.

For example:

static class Foo {

    Foo(Consumer<Runnable> runnableConsumer) {}

    Foo(BiFunction<Long, Long, Long> longAndLongToLong) {}
}

static class Bar {

    static void someMethod(Runnable runnable) {}

    static void someMethod(Integer num, String str) {}
}

There's no way Bar.someMethod() could ever satisfy longAndLongToLong , and yet the code below emits the same compile error regarding ambiguity:

new Foo(Bar::someMethod);

Holger's answer explains the logic and pertinent clause in the JLS behind this rather well.

What about binary compatibility?

Consider if the longAndLongToLong version of Foo constructor didn't exist but was added later in a library update, or if the two parameter version of Bar.someMethod() didn't exist but added later: Suddenly previously compiling code can break due to this.

This is an unfortunate side-effect of method overloading and similar problems have affected plain method calls even before lambdas or method references came along.

Fortunately, binary compatibility is preserved. The relevant clause is in 13.4.23. Method and Constructor Overloading :

Adding new methods or constructors that overload existing methods or constructors does not break compatibility with pre-existing binaries. The signature to be used for each invocation was determined when these existing binaries were compiled; ....

While adding a new overloaded method or constructor may cause a compile-time error the next time a class or interface is compiled because there is no method or constructor that is most specific (§15.12.2.5), no such error occurs when a program is executed, because no overload resolution is done at execution time.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM