Reference Is Ambiguous with Generics

Reference is ambiguous with generics

JDK is right. The 2nd method is not more specific than the 1st. From JLS3#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 type error."

This is clearly not the case here. I emphasized any invocation. The property of one method being more specific than the other purely depends on the two methods themselves; it doesn't change per invocation.

Formal analysis on your problem: is m2 more specific than m1?

m1: <R> void setValue(Parameter<R> parameter, R value) 
m2: <V> void setValue(Parameter<V> parameter, Field<V> value)

First, compiler needs to infer R from the initial constraints:

Parameter<V>   <<   Parameter<R>
Field<V> << R

The result is R=V, per inference rules in 15.12.2.7

Now we substitute R and check subtype relations

Parameter<V>   <:   Parameter<V>
Field<V> <: V

The 2nd line does not hold, per subtyping rules in 4.10.2. So m2 is not more specific than m1.

V is not Object in this analysis; the analysis considers all possible values of V.

I would suggest to use different method names. Overloading is never a necessity.


This appears to be a significant bug in Eclipse. The spec quite clearly indicates that the type variables are not substituted in this step. Eclipse apparently does type variable substitution first, then check method specificity relation.

If such behavior is more "sensible" in some examples, it is not in other examples. Say,

m1: <T extends Object> void check(List<T> list, T obj) { print("1"); }
m2: <T extends Number> void check(List<T> list, T num) { print("2"); }

void test()
check( new ArrayList<Integer>(), new Integer(0) );

"Intuitively", and formally per spec, m2 is more specific than m1, and the test prints "2". However, if substitution T=Integer is done first, the two methods become identical!


for Update 2

m1: <R> void setValue(Parameter<R> parameter, R value) 
m2: <V> void setValue(Parameter<V> parameter, Field<V> value)

m3: <T> void setValue2(Parameter<T> parameter, Field<T> value)
s4: setValue(parameter, value)

Here, m1 is not applicable for method invocation s4, so m2 is the only choice.

Per 15.12.2.2, to see if m1 is applicable for s4, first, type inference is carried out, to the conclusion that R=T; then we check Ai :< Si, which leads to Field<T> <: T, which is false.

This is consistent with the previous analysis - if m1 is applicable to s4, then any invocation handled by m2 (essentially same as s4) can be handled by m1, which means m2 would be more specific than m1, which is false.

in a parameterized type

Consider the following code

class PF<T>
{
public void setValue(Parameter<T> parameter, T value) {
}

public void setValue(Parameter<T> parameter, Field<T> value) {
}
}

void test()

PF<Object> pf2 = null;

Parameter<Object> p2 = getP2();
Field<Object> f2 = getF2();

pf2.setValue(p2,f2);

This compiles without problem. Per 4.5.2, the types of the methods in PF<Object> are methods in PF<T> with substitution T=Object. That is, the methods of pf2 are

    public void setValue(Parameter<Object> parameter, Object value) 

public void setValue(Parameter<Object> parameter, Field<Object> value)

The 2nd method is more specific than the 1st.

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 overloaded method reference when using Any

If you really need to call the overload that accepts a Collection<T> (for example, it has some desired side effects that the other one doesn't have), then there's a solution that tells the compiler to do that.

This is similar to: How to deal with an overload resolution ambiguity of functions with generics?

Basically, you can make an unchecked cast that would limit the type argument of ComboBox so that List<String> no longer matches the signature with a vararg of the argument type and there's only the signature accepting the Collection that matches.

For that, you need to choose a new type argument, say R, that satisfies both:

  • List<R> is not a subtype of R
  • List<R> is still a subtype of Collection<R>

One can note that a type that we can use as R is String, giving this:

@Suppress("UNCHECKED_CAST")
(productType as ComboBox<String>).setItems(listOf("1","2"))

However, if the behavior of the two overloads is identical, it's simpler to pass an array to the vararg overload using *list.toTypedArray(), just as you mentioned.

Why is this Java generic method call ambiguous when only one method is valid when separate?

You're right that a smarter compiler should be able to resolve this unambiguously.

The way Java resolves method invocations is complex. It's defined by the JLS, and I make it 7500 words purely to determine how to resolve a method. Pasted into a text editor, it was 15 pages.

The general approach is:

  1. Compile-Time Step 1: Determine Type to Search (no issue here)
  2. Compile-Time Step 2: Determine Method Signature
    1. Identify Potentially Applicable Methods
    2. Phase 1: Identify Matching Arity Methods Applicable by Strict Invocation
    3. Phase 2: Identify Matching Arity Methods Applicable by Loose Invocation
    4. Phase 3: Identify Methods Applicable by Variable Arity Invocation
    5. Choosing the Most Specific Method
    6. Method Invocation Type
  3. Compile-Time Step 3: Is the Chosen Method Appropriate?

I don't understand anywhere close to all of the details and how it pertains to your specific case. If you care to dive into it then I've already linked the full spec. Hopefully this explanation is good enough for your purposes:

Ambiguousness is determined at step 2.6, but there is still a further appropriateness check at step 3. Your foo method must be failing at step 3. Your bar method never makes it that far because the compiler still considers both methods to be valid possibilities. A human can make the determination that the non-appropriateness resolves the ambiguity, but that's not order the compiler does things. I could only speculate why - performance might be a factor.

Your code is operating at the intersection of generics, overloading and method references, all three of which were introduced at different times; it's not massively surprising to me that the compiler would struggle.

Java generics ambiguous method

I think that this behaviour is adequately explained in JLS 15.12.2.5 Choosing the Most Specific Method:

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

To state this another way, one method is more specific than the other if either of these statements are true:

  • Any arguments you pass in a valid invocation of the first method can also be passed in a valid invocation of the second method.
  • Any arguments you pass in a valid invocation of the second method can also be passed in a valid invocation of the first method.

Unless the first and second methods are the same, at most one of these statements can be true.


An important point about choosing the most specific method is that this is only necessary when more than one method is applicable for the given arguments.

binder.bind(String.class, new Type<String>("x")) is not ambiguous because the <T> void bind(T, Type<T>) method is not applicable: if you pass a Type<String> to that method, the only type which can be inferred for T is String (because a Type<T> is not, say, a Type<Object>).

As such, you would have to pass a String to that method. String.class is a Class<String>, not a String, so that method is not applicable, so there is no ambiguity to resolve as only one possible method - the <T> void bind(Class<T>, Type<T>) - applies.


In the ambiguous case, we are passing a Type<Object> as the second parameter. This means that, if both overloads are applicable, the first parameter would need to be a Class<Object> and an Object respectively. Object.class is indeed both of those things, hence both overloads are applicable.

To prove that these are ambiguous overloads, we can find a counter example to refute the claim that "any invocation handled by the first method could be passed on to the other" for both methods with respect to the other.

The key word here is any: this has nothing to do with the specific arguments that are being passed in here, but is only to do with the types in the method signature.

  • The successful invocation (binder.bind(String.class, new Type<String>("x"))) couldn't invoke the bind(T, Type<T>) overload, because String.class isn't a String.
  • binder.bind("", new Type<String>("")) couldn't invoke the bind(Class<T>, Type<T>) overload, because "" is a String, not a Class<String>.

QED.

This can also be demonstrated by giving one of the methods a different name, say, bind2, and attempting to pass these parameters.

<T> void bind(Class<T> clazz, Type<T> type) { ... }
<T> void bind2(T obj, Type<T> type) { ... }

binder.bind(String.class, new Type<String>("x")); // compiles
binder.bind2(String.class, new Type<String>("x")); // doesn't compile

binder.bind("", new Type<String>("x")) // doesn't compile
binder.bind2("", new Type<String>("x")) // compiles

Giving different names removes the possibility of ambiguity, so you can directly see if the parameters are applicable.


In the 1-argument case, anything you can pass to <T> void bind(Class<T>) can also be passed to <T> void bind(T). This is because Class<T> is a subclass of Object, and the bound T degenerates to Object in the second case, so it accepts anything.

As such, <T> void bind(Class<T>) is more specific than <T> void bind(T).

Redoing the renaming demonstration above:

<T> void bind3(Class<T> clazz) { ... }
<T> void bind4(T obj) { ... }

binder.bind3(String.class); // compiles
binder.bind4(String.class); // compiles

binder.bind3("") // doesn't compile
binder.bind4("") // compiles

Obviously, the fact that String.class can be passed to both bind3 and bind4 doesn't prove there isn't a parameter that can be accepted by bind3 but not bind4. I started by stating an informal intuition, so I'll finish with the informal intuition that "really, there isn't one".



Related Topics



Leave a reply



Submit