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".


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.

Tags:

Java

Generics