Search code examples
javagenericsjava-8jls

Java 8 ambiguous method reference for generic class


The code below compiles and runs ok in Java 7 but fails to compile in Java 1.8.0 u25:

public class GenericTest {

    public static class GenericClass<T> {
        T value;

        public GenericClass(T value) {
            this.value = value;
        }
    }

    public static class SecondGenericClass<T> {
        T value;

        public SecondGenericClass(T value) {
            this.value = value;
        }
    }


    public static<T >void verifyThat(SecondGenericClass<T> actual, GenericClass<T> matcher) {
    }

    public static<T >void verifyThat(T actual, GenericClass<T> matcher) {
    }

    @Test
    public void testName() throws Exception {
        verifyThat(new SecondGenericClass<>(""), new GenericClass<>(""));
    }

}

The error message in Java 8 looks like this:

Error:(33, 9) java: reference to verifyThat is ambiguous
  both method <T>verifyThat(com.sabre.ssse.core.dsl.GenericTest.SecondGenericClass<T>,com.sabre.ssse.core.dsl.GenericTest.GenericClass<T>) in com.sabre.ssse.core.dsl.GenericTest and method <T>verifyThat(T,com.sabre.ssse.core.dsl.GenericTest.GenericClass<T>) in com.sabre.ssse.core.dsl.GenericTest match

I've reviewed all the changes between:
https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.12.2
https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.12.2

But I failed to notice the exact reason for this behaviour.

Edit:

Just to answer some comments, it's quite clear that the compiler in both Java 7 and 8 will be able to handle such invocations (with signatures similar to what's left after compile time type erasure:

public static void verifyThat(SecondGenericClass actual, GenericClass matcher) {
}

public static void verifyThat(Object actual, GenericClass matcher) {
}

@Test
public void testName() throws Exception {
    verifyThat(new SecondGenericClass<>(""), new GenericClass<>(""));
}

The bytecode generated for both generic methods, and erased is the same, and looks like this:

public static verifyThat(Lcom/sabre/ssse/core/dsl/GenericTest$SecondGenericClass;Lcom/sabre/ssse/core/dsl/GenericTest$GenericClass;)V
public static verifyThat(Ljava/lang/Object;Lcom/sabre/ssse/core/dsl/GenericTest$GenericClass;)V

Edit2:

Compilation under javac 1.8.0_40 fails with the same error


Solution

  • JLS, chapter §15.12.2.5 Choosing the Most Specific Method is a hard read but contains an interesting summary:

    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 type error.

    We can easily disprove this for your case with the following example:

    GenericTest.<String>verifyThat( // invokes the first method
        new SecondGenericClass<>(""), new GenericClass<>(""));
    GenericTest.<SecondGenericClass<String>>verifyThat( // invokes the second
        new SecondGenericClass<>(""), new GenericClass<>(null));
    

    so there is no most specific method here, however, as the example shows, it is possible to invoke either method using arguments that make the other method inapplicable.

    In Java 7 it was easier to make a method inapplicable due to the limited attempts (of the compiler) to find type arguments to make more methods applicable (aka limited type inference). The expression new SecondGenericClass<>("") had the type SecondGenericClass<String> inferred from its argument "" and that’s it. So for the invocation verifyThat(new SecondGenericClass<>(""), new GenericClass<>("")) the arguments had the type SecondGenericClass<String> and GenericClass<String> which made the method <T> void verifyThat(T,GenericClass<T>) inapplicable.

    Note that there is an example of an ambiguous invocation which exhibits the ambiguity under Java 7 (and even Java 6): verifyThat(null, null); will provoke a compiler error when using javac.

    But Java 8 has Invocation Applicability Inference (there we have a difference to JLS 7, an entirely new chapter…) which allows the compiler to choose type arguments which make a method candidate applicable (which works through nested invocations). You can find such type arguments for your special case, you can even find a type argument which fits both,

    GenericTest.<Object>verifyThat(new SecondGenericClass<>(""), new GenericClass<>(""));
    

    is unambiguously ambiguous (in Java 8), even Eclipse agrees on that. In contrast, the invocation

    verifyThat(new SecondGenericClass<>(""), new GenericClass<String>(""));
    

    is specific enough to render the second method inapplicable and invoke the first method, which gives us a hint about what’s going on in Java 7 where the type of new GenericClass<>("") is fixed as GenericClass<String> just like with new GenericClass<String>("").


    The bottom line is, it’s not the choosing of the most specific method which changed from Java 7 to Java 8 (significantly), but the applicability due to the improved type inference. Once both methods are applicable, the invocation is ambiguous as neither method is more specific than the other.