Динамическое связывание Java и переопределение методов

Вчера у меня было двухчасовое техническое телефонное собеседование (которое я прошел, ух-ты!), Но я полностью задал следующий вопрос, касающийся динамического связывания в Java. И это вдвойне озадачивает, потому что я преподавал эту концепцию студентам, когда я был ТА, несколько лет назад, поэтому перспектива, что я дал им дезинформацию, немного тревожит...

Вот проблема, которую мне дали:

/* What is the output of the following program? */

public class Test {

  public boolean equals( Test other ) {
    System.out.println( "Inside of Test.equals" );
    return false;
  }

  public static void main( String [] args ) {
    Object t1 = new Test();
    Object t2 = new Test();
    Test t3 = new Test();
    Object o1 = new Object();

    int count = 0;
    System.out.println( count++ );// prints 0
    t1.equals( t2 ) ;
    System.out.println( count++ );// prints 1
    t1.equals( t3 );
    System.out.println( count++ );// prints 2
    t3.equals( o1 );
    System.out.println( count++ );// prints 3
    t3.equals(t3);
    System.out.println( count++ );// prints 4
    t3.equals(t2);
  }
}

Я утверждал, что на выходе должны быть два отдельных оператора печати из переопределенного equals() метод: в t1.equals(t3) а также t3.equals(t3), Последний случай достаточно очевиден, и с первым случаем, хотя t1 имеет ссылку на тип Object, он создается как тип Test, поэтому динамическое связывание должно вызывать переопределенную форму метода.

Очевидно нет. Мой интервьюер посоветовал мне запустить программу самому, и вот, из переопределенного метода был только один выход: в строке t3.equals(t3),

Тогда мой вопрос: почему? Как я уже упоминал, хотя t1 является ссылкой на тип Object (поэтому статическая привязка будет вызывать Object equals() метод), динамическое связывание должно позаботиться о вызове наиболее конкретной версии метода, основанной на конкретизированном типе ссылки. Что мне не хватает?

13 ответов

Решение

Java использует статическое связывание для перегруженных методов и динамическое связывание для переопределенных. В вашем примере метод equals перегружен (имеет другой тип параметра, чем Object.equals()), поэтому вызываемый метод привязан к ссылочному типу во время компиляции.

Некоторое обсуждение здесь

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

Изменить: хорошее описание здесь. В этом примере показана похожая проблема, связанная с типом параметра, но вызванная той же проблемой.

Я полагаю, что если бы привязка была на самом деле динамической, то любой случай, когда вызывающая сторона и параметр были экземпляром Test, привели бы к вызову переопределенного метода. Таким образом, t3.equals(o1) будет единственным случаем, который не будет напечатан.

equals метод Test не отменяет equals метод java.lang.Object, Посмотрите на тип параметра! Test класс перегружен equals с методом, который принимает Test,

Если equals метод предназначен для переопределения, он должен использовать аннотацию @Override. Это может привести к ошибке компиляции, чтобы указать на эту распространенную ошибку.

Интересно, что в коде Groovy (который может быть скомпилирован в файл класса) все вызовы, кроме одного, будут выполнять оператор print. (Тот, кто сравнивает Test с объектом, явно не будет вызывать функцию Test.equals(Test).) Это потому, что groovy DOES выполняет полностью динамическую типизацию. Это особенно интересно, потому что в нем нет переменных, которые явно динамически типизированы. Я читал в нескольких местах, что это считается вредным, так как программисты ожидают, что Groovy сделает что-то Java.

Java не поддерживает ковариацию в параметрах, только в возвращаемых типах.

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

Если ваш параметр для equals в Object - это Object, помещение равных с чем-либо еще в подкласс будет перегруженным, а не переопределенным методом. Следовательно, единственная ситуация, когда этот метод будет вызываться, - это когда статическим типом параметра является Test, как в случае T3.

Удачи в процессе собеседования! Я бы хотел пройти собеседование в компании, которая задает такие вопросы вместо обычных вопросов о алгоритмах / структурах данных, которые я преподаю своим студентам.

Метод перегружен, а не переопределен. Равные всегда принимают Объект в качестве параметра.

Кстати, у вас есть предмет об этом в эффективной Java Блоха (который вы должны владеть).

Некоторые примечания в динамическом связывании (DD) и статическом связывании(SB) после поиска некоторое время:

1. Время выполнения: (Ref.1)

  • DB: во время выполнения
  • SB: время компиляции

2. Используется для:

  • DB: переопределение
  • SB: перегрузка (статическая, частная, конечная) (ссылка 2)

Ссылка:

  1. Выполните средство разрешения проблем, какой метод предпочитаете использовать
  2. Потому что нельзя переопределять метод с модификатором static, private или final
  3. http://javarevisited.blogspot.com/2012/03/what-is-static-and-dynamic-binding-in.html

Я думаю, что ключ кроется в том, что метод equals() не соответствует стандарту: он принимает другой объект Test, а не объект Object, и, следовательно, не переопределяет метод equals(). Это означает, что вы на самом деле перегрузили его только для того, чтобы сделать что-то особенное, когда ему дан объект Test, а объект Object вызывает Object.equals(Object o). Просматривая этот код в любой IDE, вы увидите два метода equals() для Test.

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

/ * Каков вывод следующей программы? */

public class DynamicBinding {
    public boolean equals(Test other) {
        System.out.println("Inside of Test.equals");
        return false;
    }

    @Override
    public boolean equals(Object other) {
        System.out.println("Inside @override: this is dynamic binding");
        return false;
    }

    public static void main(String[] args) {
        Object t1 = new Test();
        Object t2 = new Test();
        Test t3 = new Test();
        Object o1 = new Object();

        int count = 0;
        System.out.println(count++);// prints 0
        t1.equals(t2);
        System.out.println(count++);// prints 1
        t1.equals(t3);
        System.out.println(count++);// prints 2
        t3.equals(o1);
        System.out.println(count++);// prints 3
        t3.equals(t3);
        System.out.println(count++);// prints 4
        t3.equals(t2);
    }
}

Я нашел интересную статью о динамическом и статическом связывании. Он поставляется с куском кода для имитации динамического связывания. Это сделало мой код более читабельным.

https://sites.google.com/site/jeffhartkopf/covariance

Ответ на вопрос "почему?" Вот как определяется язык Java.

Процитирую статью в Википедии о ковариантности и контравариантности:

Ковариация возвращаемого типа реализована в языке программирования Java версии J2SE 5.0. Типы параметров должны быть одинаковыми (инвариантными) для переопределения метода, в противном случае метод перегружается параллельным определением.

Другие языки разные.

См. Также этот вопрос SO, тесно связанный: переопределение метода JAVA equals

Это очень ясно, что здесь нет концепции переопределения. Это перегрузка метода. Object() метод класса Object принимает параметр ссылки типа Object и это equal() Метод принимает параметр ссылки типа Test.

Я попытаюсь объяснить это на двух примерах, которые являются расширенными версиями некоторых примеров, с которыми я сталкивался в Интернете.

public class Test {

    public boolean equals(Test other) {
        System.out.println("Inside of Test.equals");
        return false;
    }

    @Override
    public boolean equals(Object other) {
        System.out.println("Inside of Test.equals ot type Object");
        return false;
    }

    public static void main(String[] args) {
        Object t1 = new Test();
        Object t2 = new Test();
        Test t3 = new Test();
        Object o1 = new Object();

        int count = 0;
        System.out.println(count++); // prints 0
        o1.equals(t2);

        System.out.println("\n" + count++); // prints 1
        o1.equals(t3);

        System.out.println("\n" + count++);// prints 2
        t1.equals(t2);

        System.out.println("\n" + count++);// prints 3
        t1.equals(t3);

        System.out.println("\n" + count++);// prints 4
        t3.equals(o1);

        System.out.println("\n" + count++);// prints 5
        t3.equals(t3);

        System.out.println("\n" + count++);// prints 6
        t3.equals(t2);
    }
}

Здесь для строк со счетными значениями 0, 1, 2 и 3; у нас есть ссылка на объект для o1 и t1 на equals() метод. Таким образом, во время компиляции equals() Метод из файла Object.class будет ограничен.

Однако, хотя ссылка t1 является Object, она имеет инициализацию класса Test.
Object t1 = new Test();,
Поэтому во время выполнения он вызывает public boolean equals(Object other) который является

переопределенный метод

,

Теперь для значений счетчика, равных 4 и 6, опять же просто, что t3, который имеет ссылку и инициализацию Test, вызывает equals() метод с параметром в качестве ссылки на объект и является

перегруженный метод

ХОРОШО!

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

Другие вопросы по тегам