Java Type Inference: Reference Is Ambiguous in Java 8, But Not Java 7

Java type inference: reference is ambiguous in Java 8, but not Java 7

The problem is that the type inference has been improved. You have a method like

public <T extends Base> T get() {
return (T) new Derived();
}

which basically says, “the caller can decide what subclass of Base I return”, which is obvious nonsense. Every compiler should give you an unchecked warning about your type cast (T) here.

Now you have a method call:

set(new Derived(), new Consumer().get());

Recall that your method Consumer.get() says “the caller can decide what I return”. So it’s perfectly correct to assume that there could be a type which extends Base and implement Collection at the same time. So the compiler says “I don’t know whether to call set(Base i, Derived b) or set(Derived d, Collection<? extends Consumer> o)”.

You can “fix” it by calling set(new Derived(), new Consumer().<Derived>get()); but to illustrate the madness of your method, note that you can also change it to

public <X extends Base&Collection<Consumer>> void test() {
set(new Derived(), new Consumer().<X>get());
}

which will now call set(Derived d, Collection<? extends Consumer> o) without any compiler warning. The actual unsafe operation happened inside the get method.

So the correct fix would be to remove the type parameter from the get method and declare what it really returns, Derived.


By the way, what irritates me, is your claim that this code could be compiled under Java 7. Its limited type inference with nested method calls leads to treating the get method in a nested invocation context like returning Base which can’t be passed to a method expecting a Derived. As a consequence, trying to compile this code using a conforming Java 7 compiler will fail as well, but for different reasons.

Java 8: Reference to [method] is ambiguous

Your problem is a side-effect of Generalized Target-type Inference, an improvement in Java 8.

What is Target-type Inference

Let's take your example method,

public static <R> R get(String d) {
return (R)d;
}

Now, in the method above, the generic parameter R cannot be resolved by the compiler because there's no parameter with R.

So, they introduced a concept called Target-type Inference, which allows the parameter to be inferred based on the assignment parameter.

So, if you do,

 String str = get("something"); // R is inferred as String here
Number num = get("something"); // R is inferred as Number here

This works well in Java 7. But the following does not,

put(get("something");
static void Put(String str) {} //put method

Because type inference worked only for direct assignments.

If there's no direct assignment, then the generic type was inferred as Object.

So, when you compiled the code with Java 7, your put(Object) method was called without any problems.

What they did in Java 8

They improved the type inference to infer the type from method calls and chained method calls

More details about them here and here

So now, you can directly call put(get("something")) and the generic type will be inferred based on the parameter of the put() method.

But as you know, the methods, put(Charsequence) and put(char[]) match the arguments. So there's the ambiguity.

Fix?

Just tell the compiler exactly what you want,

put(TestClass.<CharSequence>get("hello")); // This will call the put(CharSequence) method.

Java 8 ambiguous method reference for generic class

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.

Ambiguous method in Java 8, why?

Consider the following class:

public class Foo extends Exception implements CharSequence {
//...
}

The class Foo implements both Throwable and CharSequence. So in case E is set to this instance, the Java compiler does not know which method to call.

The reason there is probably no problem for Java7 is that generics are less implemented. In case you don't provide an E yourself (e.g. (Foo) bar()), Java will fall back on the basic verion of E which is implements Exception, E is thus only considered to be an instance of Exception.

In Java8, the type inference is improved, the type of E is now derived from the parameter called for by then(), in other words the compiler first looks what possible types then() needs, the problem is that they both are valid choices. So in that case it becomes ambiguous.


Proof of concept:

Now we will slightly modify your code and show how ambiguous calls are resolved:

Say we modify the code to:

public class Main {
public static void main(String... args){
then(bar()); // Compilation Error
}
public static <E extends Exception> E bar() {
return null;
}
public static void then(CharSequence actual) {
System.out.println("char");
}
}

If you run this in Java8, there is no problem (it prints char), because Java8 simply assumes there is such class Foo (it created some kind of "inner" type for it that derived from both).

Running this in Java7 yields problems:

/MyClass.java:18: error: method then in class MyClass cannot be applied to given types;
then(bar()); // Compilation Error
^
required: CharSequence
found: Exception
reason: actual argument Exception cannot be converted to CharSequence by method invocation conversion
1 error

It did a fallback on Exception and couldn't find a type that could deal with it.

If you run the original code in Java8, it will error because of the ambiguous call, if you run it in Java7 however, it will use the Throwable method.


In short: the compiler aims to "guess" what E is in Java8, whereas in Java7 the most conservative type was picked.

Why does the Java 8 generic type inference pick this overload?

The rules of type inference have received a significant overhaul in Java 8; most notably target type inference has been much improved. So, whereas before Java 8 the method argument site did not receive any inference, defaulting to Object, in Java 8 the most specific applicable type is inferred, in this case String. The JLS for Java 8 introduced a new chapter Chapter 18. Type Inference that's missing in JLS for Java 7.

Earlier versions of JDK 1.8 (up until 1.8.0_25) had a bug related to overloaded methods resolution when the compiler successfully compiled code which according to JLS should have produced ambiguity error Why is this method overloading ambiguous? As Marco13 points out in the comments

This part of the JLS is probably the most complicated one

which explains the bugs in earlier versions of JDK 1.8 and also the compatibility issue that you see.


As shown in the example from the Java Tutoral (Type Inference)

Consider the following method:

void processStringList(List<String> stringList) {
// process stringList
}

Suppose you want to invoke the method processStringList with an empty list. In Java SE 7, the following statement does not compile:

processStringList(Collections.emptyList());

The Java SE 7 compiler generates an error message similar to the following:

List<Object> cannot be converted to List<String>

The compiler requires a value for the type argument T so it starts with the value Object. Consequently, the invocation of Collections.emptyList returns a value of type List, which is incompatible with the method processStringList. Thus, in Java SE 7, you must specify the value of the value of the type argument as follows:

processStringList(Collections.<String>emptyList());

This is no longer necessary in Java SE 8. The notion of what is a target type has been expanded to include method arguments, such as the argument to the method processStringList. In this case, processStringList requires an argument of type List

Collections.emptyList() is a generic method similar to the get() method from the question. In Java 7 the print(String string) method is not even applicable to the method invocation thus it doesn't take part in the overload resolution process. Whereas in Java 8 both methods are applicable.

This incompatibility is worth mentioning in the Compatibility Guide for JDK 8.


You can check out my answer for a similar question related to overloaded methods resolution Method overload ambiguity with Java 8 ternary conditional and unboxed primitives

According to JLS 15.12.2.5 Choosing the Most Specific Method:

If more than one member method is both accessible and applicable to a
method invocation, it is necessary to choose one to provide the
descriptor for the run-time method dispatch. The Java programming
language uses the rule that the most specific method is chosen.

Then:

One applicable method m1 is more specific than another applicable
method m2, for an invocation with argument expressions e1, ..., ek, if
any of the following are true:

  1. m2 is generic, and m1 is inferred to be more specific than m2 for
    argument expressions e1, ..., ek by §18.5.4.

  2. m2 is not generic, and m1 and m2 are applicable by strict or loose
    invocation, and where m1 has formal parameter types S1, ..., Sn and m2
    has formal parameter types T1, ..., Tn, the type Si is more specific
    than Ti for argument ei for all i (1 ≤ i ≤ n, n = k).

  3. m2 is not generic, and m1 and m2 are applicable by variable arity
    invocation, and where the first k variable arity parameter types of m1
    are S1, ..., Sk and the first k variable arity parameter types of m2
    are T1, ..., Tk, the type Si is more specific than Ti for argument ei
    for all i (1 ≤ i ≤ k). Additionally, if m2 has k+1 parameters, then
    the k+1'th variable arity parameter type of m1 is a subtype of the
    k+1'th variable arity parameter type of m2.


The above conditions are the only circumstances under which one method may be more specific than another.

A type S is more specific than a type T for any expression if S <: T (§4.10).

The second of three options matches our case. Since String is a subtype of Object (String <: Object) it is more specific. Thus the method itself is more specific. Following the JLS this method is also strictly more specific and most specific and is chosen by the compiler.



Related Topics



Leave a reply



Submit