I've noticed a weird behavior for overloading methods with generics and lambdas. This class works fine:
public <T> void test(T t) { }
public <T> void test(Supplier<T> t) { }
public void test() {
test("test");
test(() -> "test");
}
No ambiguous method call. However, changing it to this makes the second call ambiguous:
public <T> void test(Class<T> c, T t) { }
public <T> void test(Class<T> c, Supplier<T> t) { }
public void test() {
test(String.class, "test");
test(String.class, () -> "test"); // this line does not compile
}
How can this be? Why would adding another argument cause the method resolution to be ambiguous? Why can it tell the difference between a Supplier and an Object in the first example, but not the second?
Edit: This is using 1.8.0_121. This is the full error message:
error: reference to test is ambiguous
test(String.class, () -> "test");
^
both method <T#1>test(Class<T#1>,T#1) in TestFoo and method <T#2>test(Class<T#2>,Supplier<T#2>) in TestFoo match
where T#1,T#2 are type-variables:
T#1 extends Object declared in method <T#1>test(Class<T#1>,T#1)
T#2 extends Object declared in method <T#2>test(Class<T#2>,Supplier<T#2>)
/workspace/com/test/TestFoo.java:14: error: incompatible types: cannot infer type-variable(s) T
test(String.class, () -> "test");
^
(argument mismatch; String is not a functional interface)
where T is a type-variable:
T extends Object declared in method <T>test(Class<T>,T)
This problem has kept buzzing me since some days now.
I won't go into JLS details but more on a logical explanation.
What if i'm calling:
Which method the compiler can choose
Callable is a @FunctionalInterface so it seems perfectly valid
Supplier is a @FunctionalInterface so it seems also perfectly valid
When providing a class which is a FunctionalInterface then the two calls are valid and neither of the calls are more specific than the other which lead to an ambiguous method message.
Then what about the functions with only one parameter:
What if i'm calling:
Which method the compiler can choose
A lambda expression has to be mapped to a @FunctionalInterface but in this case we aren't providing any explicit type to map so this call can't be valid
Supplier is a @FunctionalInterface so the lambda expression can be mapped to a @FunctionalInterface (Supplier), the call is then valid
Only one of the two methods is applicable
During compilation all generic types are erased, so Object is used and an Object can be a FunctionalInterface
I hope someone more JLS compliant than me can validate or correct my explanation it would be great.
If my understanding of chapters 15 and 18 of the JLS for Java SE 8 is correct, the key to your question lies in the following quote from paragraph 15.12.2:
When a Java compiler encounters a method call expression such as
test(() -> "test")
, it has to search for accessible (visible) and applicable (i.e. with matching signature) methods to which this method call can be dispatched. In your first example, both<T> void test(T)
and<T> void test(Supplier<T>)
are accessible and applicable w.r.t. thetest(() -> "test")
method call. In such cases, when there are multiple matching methods, the compiler attempts to determine the most specific one. Now, while this determination for generic methods (as covered in JLS 15.12.2.5 and JLS 18.5.4) is quite complicated, we can use the intuition from the opening of 15.12.2.5:Since for any valid call to
<T> void test(Supplier<T>)
we can find a corresponding instantiation of the type parameterT
in<T> void test(T)
, the former is more specific than the latter.Now, the surprising part is that in your second example, both
<T> void test(Class<T>, Supplier<T>)
and<T> void test(Class<T>, T)
are considered applicable for method calltest(String.class, () -> "test")
, even though it's clear to us, that the latter shouldn't be. The problem is, that the compiler acts very conservatively in the presence of implicitly typed lambdas, as quoted above. See in particular JLS 18.5.1:and JLS 15.12.2.2:
So, the constraints from implicitly typed lambdas passed as arguments take no part in resolving type inference in the context of method applicability checks.
Now, if we assume that both methods are applicable, the problem - and the difference between this and the previous example - is that none of this methods is more specific. There exist calls which are valid for
<T> void test(Class<T>, Supplier<T>)
but not for<T> void test(Class<T>, T)
, and vice versa.This also explains why
test(String.class, (Supplier<String>) () -> "test");
compiles, as mentioned by @Aominè in the comment above.(Supplier<String>) () -> "test")
is an explicitly typed lambda, and as such is considered pertinent to applicability, the compiler is able to correctly deduce, that only one of these methods is applicable, and no conflict occurs.