Сегодня я столкнулся с интересной (и очень расстраивающей) проблемой с методом equals(), которая вызвала сбой того, что я считал хорошо протестированным классом, и возникла ошибка, на отслеживание которой у меня ушло очень много времени.
Для полноты картины я не использовал IDE или отладчик - только старый добрый текстовый редактор и System.out. Времени было очень мало, и это был школьный проект.
Во всяком случае -
Я разрабатывал базовую корзину для покупок, которая могла содержать ArrayList из объектов Book. Чтобы реализовать методы addBook(), removeBook() и hasBook() для корзины, я хотел проверить, существует ли уже Book в Cart. Итак, я иду -
public boolean equals(Book b) {
... // More code here - null checks
if (b.getID() == this.getID()) return true;
else return false;
}
В тестировании все работает нормально. Я создаю 6 объектов и заполняю их данными. Сделайте много операций добавления, удаления, has () на Cart, и все работает нормально. Я читал, что вы можете либо иметь equals(TYPE var), либо equals(Object o) { (CAST) var }, но предположил, что, поскольку он работает, это не имеет большого значения.
Затем я столкнулся с проблемой - мне нужно было создать объект Book с Только и ID внутри класса Book. Никакие другие данные в него вноситься не будут. В основном следующие:
public boolean hasBook(int i) {
Book b = new Book(i);
return hasBook(b);
}
public boolean hasBook(Book b) {
// .. more code here
return this.books.contains(b);
}
Внезапно метод equals(Book b) больше не работает. Это заняло ОЧЕНЬ много времени, чтобы отследить это без хорошего отладчика и при условии, что класс Cart был должным образом протестирован и верен. После замены метода equals() на следующее:
public boolean equals(Object o) {
Book b = (Book) o;
... // The rest goes here
}
Все снова заработало. Есть ли причина, по которой метод решил не принимать параметр Book, даже если он явно был объект Book? Единственная разница, казалось, заключалась в том, что он был создан из одного и того же класса и заполнен только одним членом данных. Я очень запутался. Пожалуйста, пролейте немного света?
Это хороший урок для изучения Java и равнозначных




В Java метод equals(), унаследованный от Object, имеет следующий вид:
public boolean equals(Object other);
Другими словами, параметр должен иметь тип Object. Это называется преобладающий; ваш метод public boolean equals(Book other) выполняет то, что называется перегрузка по отношению к методу equals().
ArrayList использует переопределенные методы equals() для сравнения содержимого (например, для его методов contains() и equals()), нет перегруженных. В большей части вашего кода вызов того, который не переопределяет должным образом Object, был нормальным, но несовместимым с ArrayList.
Таким образом, неправильная переопределение метода может вызвать проблемы.
Я переопределяю каждый раз:
@Override
public boolean equals(Object other){
if (other == null) return false;
if (other == this) return true;
if (!(other instanceof MyClass)) return false;
MyClass otherMyClass = (MyClass)other;
...test other properties here...
}
Использование аннотации @Override может помочь множеству глупых ошибок.
Используйте его всякий раз, когда считаете, что переопределяете метод суперкласса или интерфейса. Таким образом, если вы сделаете это неправильно, вы получите ошибку компиляции.
Это хороший аргумент в пользу аннотации @Override ... если бы OP использовал @Override, его компилятор сказал бы ему, что он на самом деле не переопределяет метод родительского класса ...
Никогда не знал о @Override, спасибо за это! Я также хотел бы добавить, что переопределение hashCode () действительно должно было быть выполнено и, возможно, раньше обнаружило ошибку.
Некоторые IDE (например, Eclipse) могут даже автоматически создавать для вас методы equals () и hashcode () на основе переменных-членов класса.
if (!(other instanceof MyClass))return false; возвращает false, если MyClass расширяет другой класс. Но он не вернет false, если другой класс расширит MyClass. Разве equal не должен быть менее противоречивым?
При использовании instanceof предыдущий nullcheck является избыточным.
Из вопроса Я не использовал IDE или отладчик - только старый добрый текстовый редактор и System.out. Времени было очень мало, и это был школьный проект. Пункты о IDE, помогающих с @Override, подчеркивают, что использование IDE и отладчика, вероятно, даже более важно, когда время очень ограничено.
Комментарий @Robert и ответ @ Nikel8000 поднимают ОЧЕНЬ важный момент. Если ваш класс не является final, вы нарушаете договор equals() о том, что o1.equals(o2) должен возвращать true, только если o2.equals(o1) также возвращает true. если o1 является экземпляром подкласса o2, o1 instanceof o2 возвращает true (как и o1.equals(o2)), но o2 instanceof o1 возвращает false, o2.equals(o1) возвращает false, и контракт разрывается.
@Blueriver o2 instanceof o1 даже не является легальным синтаксисом. Единственная потенциальная проблема была бы в том, если бы подкласс реализовал equals() более строго.
Немного не по теме вашего вопроса, но, вероятно, все равно стоит упомянуть:
У Commons Lang есть несколько отличных методов, которые вы можете использовать для переопределения равенства и хэш-кода. Проверьте EqualsBuilder.reflectionEquals (...) и HashCodeBuilder.reflectionHashCode (...). В прошлом избавил меня от многих головных болей - хотя, конечно, если вы просто хотите сделать «равное» по идентификатору, это может не соответствовать вашим обстоятельствам.
Я также согласен с тем, что вам следует использовать аннотацию @Override всякий раз, когда вы переопределяете равенство (или любой другой метод).
Если вы пользователь eclipse, вы также можете перейти на right click -> source -> generate hashCode() and equals(),
Правильно ли я, что этот метод выполняется во время выполнения? Не будет ли у нас проблем с производительностью в случае, если мы проходим большую коллекцию с элементами, проверяя их на равенство с каким-то другим элементом из-за отражения?
Если вы используете eclipse, просто перейдите в верхнее меню
Source --> Generate equals() and hashCode()
Я согласен! Этот, о котором я никогда раньше не знал, и его создание снижает вероятность ошибок.
То же самое. Спасибо, Фред!
В IntelliJ вы найдете это в разделе Код → Создать… или Ctrl + N. :)
В Netbeans вы переходите в строку меню> Источник (или щелкните правой кнопкой мыши)> Вставить код (или Ctrl-I) и нажмите Создать равно () ...
Еще одно быстрое решение, сохраняющее шаблонный код, - Аннотация Lombok EqualsAndHashCode. Это просто, элегантно и легко настраивается. И не зависит от IDE. Например;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(of = {"errorNumber","messageCode"}) // Will only use this fields to generate equals.
public class ErrorMessage{
private long errorNumber;
private int numberOfParameters;
private Level loggingLevel;
private String messageCode;
См. Доступный опции, чтобы настроить, какие поля использовать в равных. Ломбок доступен в знаток. Просто добавьте его с областью действия при условии:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.14.8</version>
<scope>provided</scope>
</dependency>
оператор instanceOf часто используется при реализации равенства.
Это популярная ловушка!
Проблема в том, что использование instanceOf нарушает правило симметрии:
(object1.equals(object2) == true)если и только если(object2.equals(object1))
если первое равенство истинно, а объект2 является экземпляром подкласса класс, к которому принадлежит obj1, то второе равенство вернет false!
если рассматриваемый класс, к которому принадлежит ob1, объявлен как final, то этот Проблема не может возникнуть, но в целом вам следует провести тестирование следующим образом:
this.getClass() != otherObject.getClass();, если нет, вернуть false, в противном случае проверить
поля для сравнения на равенство!
См. Bloch, Эффективная Java,, пункт 8, большой раздел, в котором обсуждаются проблемы с переопределением метода equals(). Он не рекомендует использовать getClass(). Основная причина в том, что это нарушает принцип замены Лискова для подклассов, которые не влияют на равенство.
в Android Studio есть alt + insert ---> равно и hashCode
Пример:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Proveedor proveedor = (Proveedor) o;
return getId() == proveedor.getId();
}
@Override
public int hashCode() {
return getId();
}
recordId - свойство объекта
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Nai_record other = (Nai_record) obj;
if (recordId == null) {
if (other.recordId != null)
return false;
} else if (!recordId.equals(other.recordId))
return false;
return true;
}
Рассматривать:
Object obj = new Book();
obj.equals("hi");
// Oh noes! What happens now? Can't call it with a String that isn't a Book...
@Elazar Как так? obj заявлен как Object. Точка наследования заключается в том, что затем вы можете назначить Book на obj. После этого, если вы не предполагаете, что Object не должен быть сопоставим с String через equals(), этот код должен быть совершенно законным и возвращать false.
Я предлагаю именно это. Я считаю, что это довольно широко распространено.
Мне известно, что я нарушил «Контракт» относительно переопределения методов equals путем отражения, однако мне нужен был быстрый способ проверить, существует ли объект в ArrayList без использования дженериков.