Переопределение Object.equals VS Перегрузка
Чтение: эффективная Java - второе издание Джошуа Блоха
Пункт 8 - Соблюдайте генеральный договор, когда переопределение равняется состояниям:
Программисту нередко приходится писать метод equals, который выглядит следующим образом, а затем часами ломать голову над тем, почему он не работает должным образом:
[Пример кода здесь]
Проблема заключается в том, что этот метод не переопределяет Object.equals, аргумент которого имеет тип Object, но вместо этого перегружает его.
Пример кода:
public boolean equals(MyClass o) {
//...
}
Мой вопрос:
Почему строго типизированный метод equals, перегруженный, как в этом примере кода, недостаточен? В книге говорится, что перегрузка, а не переопределение - это плохо, но в ней не говорится, почему это так, или какие сценарии могут привести к сбою этого метода равенства.
3 ответа
Это происходит потому, что перегрузка метода не изменит поведение в таких местах, как коллекции или другие места, где equals(Object)
метод явно используется. Например, возьмите следующий код:
public class MyClass {
public boolean equals(MyClass m) {
return true;
}
}
Если вы положите это в нечто вроде HashSet
:
public static void main(String[] args) {
Set<MyClass> myClasses = new HashSet<>();
myClasses.add(new MyClass());
myClasses.add(new MyClass());
System.out.println(myClasses.size());
}
Это напечатает 2
не 1
даже если вы ожидаете, что все MyClass
экземпляры будут равны вашей перегрузке, и набор не добавит второй экземпляр.
Так что в основном, хотя это true
:
MyClass myClass = new MyClass();
new MyClass().equals(myClass);
Это false
:
Object o = new MyClass();
new MyClass().equals(o);
И последняя версия, которую коллекции и другие классы используют для определения равенства. Фактически, единственное место, куда это вернется true
где параметр явно является экземпляром MyClass
или один из его подтипов.
Изменить: по вашему вопросу:
Переопределение против перегрузки
Давайте начнем с разницы между переопределением и перегрузкой. При переопределении вы фактически переопределяете метод. Вы удаляете его оригинальную реализацию и фактически заменяете его своей. Итак, когда вы делаете:
@Override
public boolean equals(Object o) { ... }
Вы на самом деле повторно связать свой новый equals
реализация, чтобы заменить один из Object
(или любой суперкласс, который определил это в последний раз).
С другой стороны, когда вы делаете:
public boolean equals(MyClass m) { ... }
Вы определяете совершенно новый метод, потому что вы определяете метод с тем же именем, но с другими параметрами. когда HashSet
звонки equals
, он вызывает его в переменной типа Object
:
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
(Этот код взят из исходного кода HashMap.put
, который используется в качестве базовой реализации для HashSet.add
.)
Чтобы было понятно, единственный раз, когда он будет использовать другой equals
когда equals
метод переопределен, не перегружен. Если вы попытаетесь добавить @Override
к вашей перегруженной equals
метод, он потерпит неудачу с ошибкой компилятора, жалуясь, что он не переопределяет метод. Я могу даже объявить оба equals
методы в том же классе, потому что это перегрузка:
public class MyClass {
@Override
public boolean equals(Object o) {
return false;
}
public boolean equals(MyClass m) {
return true;
}
}
Дженерики
Что касается дженериков, equals
не является общим. Это явно занимает Object
как его тип, так что точка является спорным. Теперь предположим, что вы пытались сделать это:
public class MyGenericClass<T> {
public boolean equals(T t) {
return false;
}
}
Это не скомпилируется с сообщением:
Столкновение имен: метод equals(T) типа MyGenericClass имеет такое же стирание, что и equals(Object) типа Object, но не переопределяет его
И если вы попытаетесь @Override
Это:
public class MyGenericClass<T> {
@Override
public boolean equals(T t) {
return false;
}
}
Вы получите это вместо этого:
Метод equals(T) типа MyGenericClass должен переопределить или реализовать метод супертипа
Так что ты не можешь победить. Здесь происходит то, что Java реализует дженерики, используя стирание. Когда Java заканчивает проверку всех универсальных типов во время компиляции, все действительные объекты времени выполнения заменяются на Object
, Везде, где вы видите T
фактический байт-код содержит Object
вместо. Вот почему рефлексия плохо работает с общими классами и почему вы не можете делать такие вещи, как list instanceof List<String>
,
Это также позволяет избежать перегрузки общими типами. Если у вас есть этот класс:
public class Example<T> {
public void add(Object o) { ... }
public void add(T t) { ... }
}
Вы получите ошибки компилятора от add(T)
метод, потому что, когда классы фактически завершают компиляцию, оба метода будут иметь одинаковую сигнатуру, public void add(Object)
,
Почему строго типизированный метод equals, перегруженный, как в этом примере кода, недостаточен?
Потому что это не отменяет Object.equals
, Любой код общего назначения, который знает только о методе, объявленном в Object
(например HashMap
тестирование на равенство ключей) не в конечном итоге вызовет вашу перегрузку - они просто вызовут исходную реализацию, которая дает ссылочное равенство.
Помните, что перегрузка определяется во время компиляции, тогда как переопределение определяется во время выполнения.
Если вы переопределяете equals
часто бывает полезно предоставить строго типизированную версию и делегировать ее из метода, объявленного в equals
,
Вот полный пример того, как это может пойти не так:
import java.util.*;
final class BadKey {
private final String name;
public BadKey(String name) {
// TODO: Non-nullity validation
this.name = name;
}
@Override
public int hashCode() {
return name.hashCode();
}
public boolean equals(BadKey other) {
return other != null && other.name.equals(name);
}
}
public class Test {
public static void main(String[] args) throws Exception {
BadKey key1 = new BadKey("foo");
BadKey key2 = new BadKey("foo");
System.out.println(key1.equals(key2)); // true
Map<BadKey, String> map = new HashMap<BadKey, String>();
map.put(key1, "bar");
System.out.println(map.get(key2)); // null
}
}
Чтобы исправить это, просто добавьте переопределение, например так:
@Override
public boolean equals(Object other) {
// Delegate to the more strongly-typed implementation
// where appropriate.
return other instanceof BadKey && equals((BadKey) other);
}
Поскольку коллекции, использующие равные, будут использовать Object.equals(Object)
метод (потенциально переопределяемый в MyClass и, следовательно, вызываемый полиморфно), который отличается от MyClass.equals(MyClass)
,
Перегрузка метода определяет новый, другой метод, имя которого совпадает с именем другого.