Неоднозначный метод дженериков Java - программирование
Подтвердить что ты не робот

Неоднозначный метод дженериков Java

У меня проблемы с пониманием поведения кода ниже. Мы будем благодарны за любую помощь в понимании.

class Binder {

    <T> void bind(Class<T> clazz, Type<T> type) {
        System.out.println("clazz type");
    }

    <T> void bind(T obj, Type<T> type) {
        System.out.println("obj type");
    }
}

class Type<T> {
    Type(T obj) { }
}

Binder binder = new Binder();

binder.bind(String.class, new Type<String>("x")) //works

binder.bind(Object.class, new Type<Object>(new Object()))  //ambiguous

Приведенный выше код не будет работать с

ERROR: reference to bind is ambiguous
  both method <T>bind(java.lang.Class<T>,Type<T>) in Binder and method <T>bind(T,Type<T>) in Binder match

Если бы я удалил второй аргумент для каждого метода, оба вызова связывания выполняли бы первый метод

class Binder {

    <T> void bind(Class<T> clazz) {
        System.out.println("clazz");
    }

    <T> void bind(T obj) {
        System.out.println("obj");
    }
}

Binder binder = new Binder();

binder.bind(String.class)

binder.bind(Object.class)

Выше будет напечатано "clazz" дважды.

4b9b3361

Ответ 1

Я думаю, что это поведение адекватно объяснено в JLS 15.12.2.5 Выбор наиболее конкретного метода:

Неформальная интуиция заключается в том, что один [применимый] метод более специфичен, чем другой [применимый метод], если любой вызов, обработанный первым методом, может быть передан другому без ошибки времени компиляции.

Чтобы сформулировать это по-другому, один метод более конкретен, чем другой, если любое из этих утверждений истинно:

  • Любые аргументы, которые вы передаете в действительном вызове первого метода, также могут быть переданы в действительном вызове второго метода.
  • Любые аргументы, которые вы передаете в действительном вызове второго метода, также могут быть переданы в действительном вызове первого метода.

Если первый и второй методы не совпадают, самое большее одно из этих утверждений может быть верным.


Важным моментом при выборе наиболее конкретного метода является то, что это необходимо, только если для данных аргументов применимо более одного метода.

binder.bind(String.class, new Type<String>("x")) не является неоднозначным, потому что метод <T> void bind(T, Type<T>) не применим: если вы передадите Type<String> этому методу, единственным типом, который может быть выведен для T, является String (потому что Type<T> не является скажем, Type<Object>).

Таким образом, вы должны будете передать String этому методу. String.class - это Class<String>, а не String, поэтому этот метод неприменим, поэтому нет двусмысленности, которую можно решить, так как применяется только один возможный метод - <T> void bind(Class<T>, Type<T>).


В неоднозначном случае мы передаем Type<Object> в качестве второго параметра. Это означает, что, если применимы обе перегрузки, первый параметр должен быть Class<Object> и Object соответственно. Object.class действительно является обеими этими вещами, поэтому применимы обе перегрузки.

Чтобы доказать, что это неоднозначные перегрузки, мы можем найти контрпример, чтобы опровергнуть утверждение, что "любой вызов, обработанный первым методом, может быть передан другому" для обоих методов по отношению к другому.

Ключевое слово здесь любое: это не имеет ничего общего с конкретными аргументами, которые передаются здесь, а только с типами в сигнатуре метода.

  • Успешный вызов (binder.bind(String.class, new Type<String>("x"))) не может вызвать перегрузку bind(T, Type<T>), потому что String.class не является String.
  • binder.bind("", new Type<String>("")) не может вызвать перегрузку bind(Class<T>, Type<T>), потому что "" является String, а не Class<String>.

QED.

Это также можно продемонстрировать, дав одному из методов другое имя, скажем, bind2, и попытавшись передать эти параметры.

<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")); // does not compile

binder.bind("", new Type<String>("x")) // does not compile
binder.bind2("", new Type<String>("x")) // compiles

Задание разных имен устраняет возможность неоднозначности, поэтому вы можете непосредственно увидеть, применимы ли параметры.


В случае с 1 аргументом все, что вы можете передать в <T> void bind(Class<T>), также может быть передано в <T> void bind(T). Это связано с тем, что Class<T> является подклассом Object, а связанный T вырождается в Object во втором случае, поэтому он принимает все.

Таким образом, <T> void bind(Class<T>) более специфичен, чем <T> void bind(T).

Повтор демонстрации переименования выше:

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

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

binder.bind3("") // does not compile
binder.bind4("") // compiles

Очевидно, тот факт, что String.class может быть передан как bind3, так и bind4, не доказывает, что не существует параметра, который может быть принят bind3, но не bind4. Я начал с неформальной интуиции, поэтому я закончу с неформальной интуицией, которая "на самом деле, не одна".

Ответ 2

Позвольте мне пересмотреть системные выходы, например, для моего понимания:

public class Binder
{
    class Type<T>
    {
        Type( T obj )
        {
            System.out.println( "Type class: " + obj.getClass( ) );
        }
    }
}

Мы можем проверить каждый из этих случаев один за другим:

Как объектный вызов неоднозначен?

1) вызов тестового объекта в классе:

<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());
}

Выход:

Type class: class java.lang.Object
test clazz bind
Clazz class: class java.lang.Object

Мое объяснение:

В этом случае T выбран в качестве объекта. Так объявление функции стало как bind(Class<Object> obj, Type<Object>) это хорошо, потому что мы звоним с bind(Object.class, new Type<Object) где Object.class is assignable to Class<Object>, так что этот вызов в порядке.

2) вызов тестового объекта на 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());
}

Выход:

Type class: class java.lang.Object
test obj bind
Obj class: class java.lang.Class

Мое объяснение:

В этом случае T выбран в качестве объекта. Таким образом, объявление функции стало как bind(Object obj, Type<Object>), что хорошо, потому что мы вызываем с bind(Object.class, new Type<Object), Class<Object>, который назначается Object в качестве первого параметра.

Таким образом, оба метода подходят для вызова объекта. Но почему String Call не является двусмысленным? Давайте проверим это:

Как строковый вызов НЕ является двусмысленным?

3) Тестовый вызов строки в классе:

<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") );
}

Выход:

 Type class: class java.lang.String

 test clazz bind 

 Clazz class: class java.lang.String

Мое объяснение:

В этом случае T выбирается как String. Таким образом, объявление функции стало как bind(Class<String> clazz, Type<String> type), что хорошо, потому что мы звоним с bind(String.class, new Type<String), который можно назначить точно. Как насчет T bind?

4) Тестовый вызов строки на 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") );
}

Выход:

Ошибка компилятора

Мое объяснение:

В этом случае T выбирается как String. Таким образом, объявление функции стало как bind(String obj, Type<String> type), что НЕ нормально, потому что мы вызываем с bind(String.class, new Type<String). String.class which means Class<String>. Поэтому мы пытаемся вызвать функцию (String, Type<String>) с входами (Class, Type<String), которые нельзя назначить.