Вчера у меня было двухчасовое собеседование по телефону по техническим вопросам (которое я прошел, у-у-у!), Но я полностью замолчал следующий вопрос, касающийся динамического связывания в 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 (поэтому статическая привязка будет вызывать метод equals() объекта), динамическая привязка должен позаботится о вызове наиболее конкретной версии метода на основе созданного типа ссылки. Что мне не хватает?




Я думаю, что ключ кроется в том, что метод equals () не соответствует стандарту: он принимает другой объект Test, а не объект Object, и, таким образом, не переопределяет метод equals (). Это означает, что вы фактически перегрузили его только для того, чтобы сделать что-то особенное, когда ему дали объект Test, а объект объекта вызывает Object.equals (Object o). Просмотр этого кода в любой среде IDE должен показать вам два метода equals () для Test.
В этом и в большинстве ответов отсутствует суть. Проблема не в том, что вместо переопределения используется перегрузка. Вот почему для t1.equals (t3) не используется перегруженный метод, когда t1 объявлен как Object, но инициализирован как Test.
Метод перегружен, а не переопределен. Equals всегда принимает объект в качестве параметра.
Кстати, у вас есть пункт об этом в эффективной java Блоха (который вам должен принадлежать).
Эффективная Java Джошуа Блоха?
Эффективно, да, когда печатал, думал о другом: D
Метод equals для Test не отменяет метод equals для java.lang.Object. Посмотрите на тип параметра! Класс Test перегружает equals методом, который принимает Test.
Если метод equals предназначен для переопределения, он должен использовать аннотацию @Override. Это привело бы к ошибке компиляции, чтобы указать на эту распространенную ошибку.
Да, я не совсем понимаю, почему я упустил эту простую, но важную деталь, но именно в этом и заключалась моя проблема. Спасибо!
+1 за верный ответ на любопытные результаты вопрошающего
Пожалуйста, найдите мой пост к этому ответу, где я изо всех сил старался объяснить дополнительные случаи. Я был бы очень признателен за ваш вклад :)
Java не поддерживает ковариацию параметров, только возвращаемых типов.
Другими словами, хотя ваш тип возвращаемого значения в методе переопределения может быть подтипом того, что было в переопределенном методе, это неверно для параметров.
Если вашим параметром для equals в Object является Object, установка равенства с чем-либо еще в подклассе будет перегруженным, а не переопределенным методом. Следовательно, единственная ситуация, в которой будет вызван этот метод, - это когда статическим типом параметра является Test, как в случае T3.
Удачи в прохождении собеседования! Я бы хотел пройти собеседование в компании, которая задает подобные вопросы вместо обычных вопросов об алгоритмах / структурах данных, которым я обучаю своих студентов.
Вы имеете в виду контравариантные параметры.
Я каким-то образом полностью замалчил тот факт, что различные параметры метода по сути создают перегруженный метод, а не переопределенный. О, не волнуйтесь, тоже были вопросы об алгоритмах и структурах данных. : P И спасибо за удачу, она мне понадобится! :)
Интересно, что в коде Groovy (который может быть скомпилирован в файл класса) все вызовы, кроме одного, будут выполнять оператор печати. (Тот, который сравнивает Test с Object, явно не вызовет функцию Test.equals (Test).) Это потому, что groovy ДЕЙСТВИТЕЛЬНО выполняет полностью динамическую типизацию. Это особенно интересно, потому что в нем нет переменных, которые явно динамически типизируются. Я читал в нескольких местах, что это считается вредным, так как программисты ожидают, что groovy сможет делать Java-вещь.
К сожалению, цена, которую платит за это Groovy, - это серьезный удар по производительности, поскольку при каждом вызове метода используется отражение. Ожидать, что один язык будет работать точно так же, как другой, обычно считается вредным. Нужно знать различия.
Должно быть хорошо и быстро с invokedynamic в JDK7 (или даже с использованием аналогичной техники реализации сегодня).
Java использует статическую привязку для перегруженных методов и динамическую привязку для переопределенных. В вашем примере метод equals перегружен (имеет другой тип параметра, чем Object.equals ()), поэтому вызываемый метод привязан к типу Справка во время компиляции.
Некоторое обсуждение здесь
Тот факт, что это метод equals, на самом деле не актуален, за исключением того, что это распространенная ошибка - перегрузить, а не переопределить его, о чем вы уже знаете, основываясь на своем ответе на проблему на собеседовании.
Редактировать: Хорошее описание здесь. В этом примере показана аналогичная проблема, связанная с типом параметра, но вызванная той же проблемой.
Я считаю, что если бы привязка была действительно динамической, то в любом случае, когда вызывающий объект и параметр были экземпляром Test, был бы вызван переопределенный метод. Таким образом, t3.equals (o1) будет единственным случаем, который не будет печататься.
Многие люди отмечают, что он перегружен, а не переопределен, но даже при этом вы ожидаете, что он правильно разрешит перегруженный. Насколько я могу судить, ваш пост на самом деле пока единственный, который правильно отвечает на вопрос.
Моя ошибка заключалась в полном упущении того факта, что метод действительно перегружен, а не переопределен. Я увидел "equals ()" и сразу подумал "унаследовано и переопределено". Похоже, я снова понял более широкую и сложную концепцию правильно, но напортачил с простыми деталями. :П
другая причина существования аннотации @Override.
Повторяйте за мной: «Java использует статическую привязку для перегруженных методов и динамическую привязку для переопределенных» - +1
так что я закончил, не зная этого. Спасибо!
Пожалуйста, найдите мой пост к этому ответу, где я изо всех сил старался объяснить дополнительные случаи. Я был бы очень признателен за ваш вклад :)
Ответ на вопрос "почему?" так определяется язык Java.
Процитируем Статья в Википедии о ковариации и контравариантности:
Return type covariance is implemented in the Java programming language version J2SE 5.0. Parameter types have to be exactly the same (invariant) for method overriding, otherwise the method is overloaded with a parallel definition instead.
Остальные языки разные.
Моя проблема была примерно эквивалентна тому, что я увидел 3 + 3 и написал 9, затем увидел 1 + 1 и написал 2. Я понимаю, как определяется язык Java; в этом случае по какой-то причине я полностью принял метод за то, чем он не был, хотя я избежал этой ошибки в другом месте той же проблемы.
Совершенно очевидно, что здесь нет концепции переопределения. Это перегрузка метода.
метод Object() класса Object принимает параметр ссылки типа Object, а этот метод equal() принимает параметр ссылки типа Test.
Некоторые заметки в Динамическое связывание (DD) и Статическая привязка̣̣̣ (SB) после некоторого поиска:
1. время выполнения: (Ссылка 1)
2. используется для:
Справка:
Я нашел интересную статью о динамическом и статическом связывании. Он поставляется с фрагментом кода для имитации динамического связывания. Это сделало мой код более читабельным.
Если добавлен другой метод, который переопределяет вместо перегрузки, он объяснит вызов динамической привязки во время выполнения.
/ * Что дает следующая программа? * /
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);
}
}
Я попытаюсь объяснить это двумя примерами, которые являются расширенными версиями некоторых примеров, с которыми я столкнулся в Интернете.
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 t1 = new Test();.
Поэтому во время выполнения он вызывает public boolean equals(Object other), который является
overridden method
Теперь, для значений счетчика 4 и 6, снова очевидно, что t3, который имеет Справка и инициализация теста, вызывает метод equals() с параметром как ссылки на объекты и является
overloaded method
В ПОРЯДКЕ!
Again, to better understand what method the compiler will call, just click on the method and Eclipse will highlight the methods of similar types which it thinks will call at the compile time. If it doesn't get called at compile time then those methods are an example of method overridding.
Пожалуйста, найдите мой пост к этому ответу, где я изо всех сил старался объяснить дополнительные случаи. Я был бы очень признателен за ваш вклад :)