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 thebind(T, Type<T>)
overload, becauseString.class
isn't aString
. binder.bind("", new Type<String>(""))
couldn't invoke thebind(Class<T>, Type<T>)
overload, because""
is aString
, not aClass<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".
Let me revise System outs like those for my understanding:
public class Binder
{
class Type<T>
{
Type( T obj )
{
System.out.println( "Type class: " + obj.getClass( ) );
}
}
}
We can test each of the cases one by one:
How Object call is ambiguous?
1) Test Object call on Class:
<T> void bind( Class<T> clazz, Type<T> type )
{
System.out.println( "test clazz bind" );
System.out.println( "Clazz class: " + clazz );
}
@Test
public void bind_Object( )
{
Binder binder = new Binder( );
binder.bind(Object.class, new Type<Object>(new Object());
}
Output:
Type class: class java.lang.Object
test clazz bind
Clazz class: class java.lang.Object
My explanation:
In this case, T is chosen as Object. So function declaration became as
bind(Class<Object> obj, Type<Object>)
which is fine because we are calling with
bind(Object.class, new Type<Object)
where Object.class is assignable to Class<Object>
so this call is fine.
2) Test Object call on T:
<T> void bind( T obj, Type<T> type )
{
System.out.println( "test obj bind" );
System.out.println( "Obj class: " + obj.getClass() );
}
@Test
public void bind_Object( )
{
Binder binder = new Binder( );
binder.bind(Object.class, new Type<Object>(new Object());
}
Output:
Type class: class java.lang.Object
test obj bind
Obj class: class java.lang.Class
My explanation:
In this case T is chosen as Object. So function declaration became as bind(Object obj, Type<Object>)
which is fine because we are calling with bind(Object.class, new Type<Object), Class<Object>
is assignable to Object
as first param.
So both methods are appropriate for the Object call. But why String call is not ambiguous? Let's test it:
How String call is NOT ambiguous?
3) Test String call on Class:
<T> void bind( Class<T> clazz,Type<T> type )
{
System.out.println( "test clazz bind" );
System.out.println( "Clazz class: " + clazz );
}
@Test
public void bind_String( )
{
Binder binder = new Binder( );
binder.bind( String.class, new Type<String>( "x") );
}
Output:
Type class: class java.lang.String
test clazz bind
Clazz class: class java.lang.String
My explanation:
In this case T is chosen as String. So function declaration became as bind(Class<String> clazz, Type<String> type)
which is fine because we are calling with bind(String.class, new Type<String)
which is assignable for sure. How about T bind?
4) Test String call on T:
<T> void bind( T obj, Type<T> type )
{
System.out.println( "test obj bind" );
System.out.println( "Obj class: " + obj.getClass() );
}
@Test
public void bind_String( )
{
Binder binder = new Binder( );
binder.bind( String.class, new Type<String>( "x") );
}
Output:
Compiler error
My explanation:
In this case T is chosen as String. So function declaration became as bind(String obj, Type<String> type)
which is NOT fine because we are calling with bind(String.class, new Type<String)
. String.class which means Class<String>
. So we try to call (String, Type<String>)
function with (Class, Type<String)
inputs which is not assignable.