Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ambiguous method call when overloading method with generics and lambdas

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)
like image 955
Scott Johnson Avatar asked Jan 19 '18 20:01

Scott Johnson


1 Answers

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:

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.

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. the test(() -> "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:

The informal intuition is that one method is more specific than another if any invocation handled by the first method could be passed on to the other one without a compile-time error.

Since for any valid call to <T> void test(Supplier<T>) we can find a corresponding instantiation of the type parameter T 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 call test(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:

A set of constraint formulas, C, is constructed as follows.

...

  • To test for applicability by strict invocation:

If k ≠ n, or if there exists an i (1 ≤ i ≤ n) such that e_i is pertinent to applicability (§15.12.2.2) (...) Otherwise, C includes, for all i (1 ≤ i ≤ k) where e_i is pertinent to applicability, ‹e_i → F_i θ›.

  • To test for applicability by loose invocation:

If k ≠ n, the method is not applicable and there is no need to proceed with inference.

Otherwise, C includes, for all i (1 ≤ i ≤ k) where e_i is pertinent to applicability, ‹e_i → F_i θ›.

and JLS 15.12.2.2:

An argument expression is considered pertinent to applicability for a potentially applicable method m unless it has one of the following forms:

  • An implicitly typed lambda expression (§15.27.1).

...

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.

like image 129
korolar Avatar answered Nov 02 '22 13:11

korolar