23

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.

12
  • 2
    The place in the JLS where this should be defined is 15.27.3. (Haven't looked at it in detail). Commented Mar 28, 2015 at 22:59
  • 3
    Why do you think Function<T, ?> does not match? ? could be Void as well, so it matches. Commented Mar 28, 2015 at 23:00
  • 4
    I'd say it must be some sort of bug: When commenting out the Consumer method, it complains that it can not call the Function-method with the given lambda - thus, it can not have been ambiguous anyhow. Interesting: When declaring the argument as (int[] data) (thus, making it an explicitly typed lambda), then it properly resolves it as the Consumer version. When additionally inserting return null; in the body, it resolves to the Function version. So it obviously stumbles over the implicitly typed and void compatible lambda (as defined in the JLS). Commented Mar 29, 2015 at 0:41
  • 2
    I get the same error, but since tomse states that the code compiles under 1.8.0_25 this may be an issue specific to 1.8.0_40. Maybe try running under 1.8.0_25 to see if the code compiles? Commented Mar 29, 2015 at 7:44
  • 4
    @EddyG Given the times you think there is a javac bug versus the times there actually is one, I think it is way more appropriate to first write a question on Stackoverflow, and only then write a bug report. For the record, I did submit it a few hours ago, but I'm still awaiting whether it will be accepted. Commented Mar 30, 2015 at 12:57

2 Answers 2

13

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(); } to make a lambda expression explicitly void-compatible, you can use x->( foo() ) to make a lambda expression explicitly value-compatible.


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

Sign up to request clarification or add additional context in comments.

4 Comments

Can this also explain why I get the same error if I remove the <T> type argument and substitute it by int[] in both parameters? That would seem to solve the generics problem, yet still gives that error.
As elaborated in the linked answer (see the comparing example), the return type of the lambda expression/ method reference isn’t considered during overload resolution. Note that recent compilers will give you a warning about the potential ambiguity of the overloaded methods just at the declaration site without the need to find out via actually ambiguous invocations. You know “the target type isn’t known” applies to the compiler (adhering to the formal process) not to us human readers and doesn’t require Generics but just a strict ordering of the resolve steps.
The x->( foo() ) workaround is great. However can you explain what exactly the round brackets mean in this context? Is that lambda syntax? Or is that "normal" wrapping of a java statement?
@mkurz it's a normal wrapping of an expression, as foo() can be an expression or a statement whereas (foo()) can only be an expression. E.g., you can write var result = (foo()); but you can not write (foo()); as a statement. Likewise, { foo(); } can only be a statement, as you can write it where a statement is expected, but you can not write var result = { foo(); }.
0

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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.