11

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

Flow
  • 23,572
  • 15
  • 99
  • 156
xendoo
  • 113
  • 7
  • even with reified generics this would be an issue – Jonathan Schneider Apr 07 '15 at 14:19
  • Possible duplicate: http://stackoverflow.com/questions/28466925/java-type-inference-reference-is-ambiguous-in-java-8-but-not-java-7 – assylias Apr 07 '15 at 14:21
  • yes, but according to JLS, the more specific method should be chosen – xendoo Apr 07 '15 at 14:21
  • 1
    @xendoo the problem occurs at runtime. Then, the generic types are erased and it's not possible to determine a difference/ both methods are equally inspecific. – SME_Dev Apr 07 '15 at 14:35
  • @SME_Dev `reference to verifyThat is ambiguous` does look like a compiler warning and not like a runtime problem(/exception). – Flow Apr 07 '15 at 14:50
  • @xendoo Could you minimize the example? Is the second parameter of `verifyThat` required to reproduce the different behavior between Java7 and Java8? – Flow Apr 07 '15 at 15:00
  • @Flow yes, it is necessary, for methods with single parameter compilations works fine `public staticvoid verifyThat(SecondGenericClass actual) { } public staticvoid verifyThat(T actual) { }` – xendoo Apr 07 '15 at 15:02
  • 1
    After getting on a computer and testing your code, I get no warning or errors in Eclipse Luna 4.4 using Java 8u31 - [picture](http://gyazo.com/2a71ea25fe91bf68f3cdc7eb0ea3c6ce) – Vince Apr 07 '15 at 15:09
  • @VinceEmigh Eclipse incremental compiler differs a lot from javac. I'm using IntelliJ which uses javac. – xendoo Apr 07 '15 at 15:12

2 Answers2

11

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.

Holger
  • 285,553
  • 42
  • 434
  • 765
1

In resolving which method to use in the case where multiple methods are applicable, "...the types of an invocation's arguments cannot, in general, be inputs to the analysis." The Java 7 spec is missing this qualification.

If you substitute T in the second definition of verifyThat for SecondGenericClass the signatures match.

In other words, imagine attempting to call the second definition of verifyThat like this:

SecondGenericClass<String> t = new SecondGenericClass<String>("foo");
GenericTest.verifyThat(t, new GenericClass<String>("bar"));

At runtime, there would be no way to determine which version of verifyThat to call since the type of variable t is a valid substitution for both SecondGenericClass<T> and T.

Note that if Java had reified generics (and it will someday), in this example one method signature is not more specific than the other. Closing loopholes...

Jonathan Schneider
  • 26,852
  • 13
  • 75
  • 99
  • The bytecode generated for those functions looks like that, and according to the jls, compiler should use more specific method if one of the parameters is subtype of another: `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`
    – xendoo Apr 07 '15 at 14:17
  • 15.12.2.5. Choosing the Most Specific Method states: _"The Java programming language uses the rule that the most specific method is chosen"_ ... _"A type S is more specific than a type T for any expression if S <: T "_ ... – xendoo Apr 07 '15 at 14:25
  • by all accounts, Java 7 type inference was weaker in this respect. if you are looking at a Java 7 spec, that may be the source of the confusion. – Jonathan Schneider Apr 07 '15 at 14:26
  • I'm looking at Java 8 spec, and by the way, the type inference here is weaker in Java 8, it finds two methods applicable, while Java 7 finds only one. – xendoo Apr 07 '15 at 14:28
  • Java 7 would have given you a runtime time warning I believe, Java 8 is preventing it at compile time. The examples in the spec all point to concrete types, e.g., ColoredPoint vs. Point, and not to generics. – Jonathan Schneider Apr 07 '15 at 14:30
  • 1
    "To check for applicability, the types of an invocation's arguments cannot, in general, be inputs to the analysis." – Jonathan Schneider Apr 07 '15 at 14:32
  • Note that the Java 7 spec does not have this qualification. – Jonathan Schneider Apr 07 '15 at 14:34
  • _"Instead, the input to the applicability check is a list of argument expressions, which can be checked for compatibility with potential target types, even if the ultimate types of the expressions are unknown."_ In this case the expression passed as argument can be easily used to resolve the compatibility with the signature – xendoo Apr 07 '15 at 14:38
  • I believe this is a reference to the types of the supplied arguments and not of the method parameter type definitions. – Jonathan Schneider Apr 07 '15 at 14:40
  • 1
    @xendo `S <: T` indicates [proper subtype](https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.10), and both `SecondGeneric` and `Object` are proper supertypes. `S <1 T` is for direct subtypes – Vince Apr 07 '15 at 14:47
  • 1
    I updated my question with example of similar functions after erasing the generic type T, bytecode generated for them is identical to previous functions. But this time compilation succedes in both Java 7 and 8 – xendoo Apr 07 '15 at 14:48
  • when they do reify generics in Java, your new code will still be valid, the generic form will not. – Jonathan Schneider Apr 07 '15 at 14:52
  • 1
    @VinceEmigh still, the SecondGeneric is proper subtype of Object, and should be used to choose more specific method – xendoo Apr 07 '15 at 14:55
  • @jkschneider FYI, Java 7 does not return any runtime/compile time warnings – xendoo Apr 07 '15 at 14:56
  • @VinceEmigh I assume that you are using eclipse compiler? – xendoo Apr 07 '15 at 15:08
  • 3
    This is far away from being a valid explanation. At runtime, the type of `t` doesn’t matter as the selection of the method happens at *compile-time*. Ranting about type erasure doesn’t really help; there wouldn’t be any difference with reified types . – Holger Apr 07 '15 at 15:25