Переопределение Java-метода equals () - не работает?

150

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 объектом? Казалось, единственное отличие состоит в том, что он был создан из одного и того же класса и заполнен только одним элементом данных. Я очень, очень смущен. Пожалуйста, пролить немного света?

Джош Смитон
источник
1
Мне известно, что я нарушил «Контракт», касающийся переопределения методов equals, потому что был рефлексивным - однако мне нужен был быстрый способ проверить, существует ли объект в ArrayList, без использования обобщений.
Джош Смитон
1
Это хороший урок, чтобы узнать о Java и равных
jjnguy

Ответы:

329

В Java equals()метод, который наследуется от Object:

public boolean equals(Object other);

Другими словами, параметр должен иметь тип Object. Это называется переопределением ; ваш метод public boolean equals(Book other)делает то , что называется перегрузкой к equals()методу.

В ArrayListиспользует переопределенные equals()методы для сравнения содержимого (например , для его contains()и equals()методов), не перегруженных из них. В большей части вашего кода вызывать тот, который не корректно переопределяет Objectequals, было хорошо, но не совместимо с 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аннотации может помочь тонне с глупыми ошибками.

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

jjnguy
источник
31
Это хороший аргумент в пользу аннотации @Override ... если бы OP использовал @Override, его компилятор сказал бы ему, что он на самом деле не переопределяет метод родительского класса ...
Коуэн
1
Никогда не знал о @ Override, спасибо за это! Я также хотел бы добавить, что переопределение hashCode () действительно должно было быть сделано и, возможно, раньше обнаружило ошибку.
Джош Смитон
5
Некоторые IDE (например, Eclipse) могут даже автоматически генерировать для вас методы equals () и hashcode () на основе переменных члена класса.
ск.
1
if (!(other instanceof MyClass))return false;возвращает falseif MyClassрасширяет другой класс. Но он не вернется, falseесли другой класс расширится MyClass. Не должно equalбыть менее противоречивым?
Роберт
19
При использовании instanceof предыдущая проверка нуля является избыточной.
Матеуш Дымчик,
108

Если вы используете Eclipse, просто перейдите в верхнее меню

Source -> Generate equals () и hashCode ()

Фред
источник
Я согласен! Этот, о котором я никогда не знал, и его создание делает его менее подверженным ошибкам
Мальчик
Тоже самое. Спасибо, Фред!
Анила
16
В IntelliJ вы найдете это в Code → Generate… или control + N. :)
правильно,
В Netbeans вы идете в строку меню> «Источник» (или щелкните правой кнопкой мыши)> «Вставить код» (или Ctrl-I) и нажмите «Создать равно () ...»
Соломон,
11

Немного не по теме, но, вероятно, стоит упомянуть:

У Commons Lang есть несколько превосходных методов, которые вы можете использовать в переопределении equals и hashcode. Проверьте EqualsBuilder.reflectionEquals (...) и HashCodeBuilder.reflectionHashCode (...) . В прошлом я избавил меня от головной боли - хотя, конечно, если вы просто хотите сделать «равные» по ID, это может не соответствовать вашим обстоятельствам.

Я также согласен с тем, что вам следует использовать @Overrideаннотацию всякий раз, когда вы переопределяете «равно» (или любой другой метод).


источник
4
Если вы являетесь пользователем затмения, вы также можете пойти right click -> source -> generate hashCode() and equals(),
tunaranch
1
Я прав, что этот метод выполняется во время выполнения? Не будут ли у нас проблемы с производительностью в случае, если мы проходим большую коллекцию с элементами, проверяя их на предмет равенства с другими элементами из-за отражения?
Гакет
4

Другим быстрым решением, которое сохраняет стандартный код, является аннотация 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;

Посмотрите доступные опции, чтобы настроить, какие поля использовать в равных. Ломбок avalaible в мавена . Просто добавьте его с предоставленной областью:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.14.8</version>
    <scope>provided</scope>
</dependency>
borjab
источник
1

в Android Studio есть alt + insert ---> equals и 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();
}
Дэвид Хакро
источник
1

Рассматривать:

Object obj = new Book();
obj.equals("hi");
// Oh noes! What happens now? Can't call it with a String that isn't a Book...
bcsb1001
источник
1
@Elazar Как так? objобъявлен как Object. Суть наследования в том , что вы можете назначить Bookдля obj. После этого, если вы не предполагаете, что an Objectне должен быть сопоставим с Stringvia equals(), этот код должен быть абсолютно легальным и возвращаться false.
bcsb1001
Я предлагаю именно это. Я считаю, что это довольно широко принято.
Элазар
0

instanceOfутверждение часто используется при реализации равных.

Это популярная ловушка!

Проблема в том, что использование instanceOfнарушает правило симметрии:

(object1.equals(object2) == true) если и только если (object2.equals(object1))

если первое равенство равно true, а object2 является экземпляром подкласса класса, к которому принадлежит obj1, то второе равенство вернет false!

если рассматриваемый класс, к которому принадлежит ob1, объявлен как final, то эта проблема не может возникнуть, но в целом вы должны проверить следующее:

this.getClass() != otherObject.getClass(); если нет, верните false, в противном случае проверьте поля для сравнения на равенство!

Nikel8000
источник
3
См. Bloch, Effective Java, Item 8, большой раздел, в котором обсуждаются проблемы с переопределением equals()метода. Он рекомендует против использования getClass(). Основная причина в том, что это нарушает принцип подстановки Лискова для подклассов, которые не влияют на равенство.
Стюарт Маркс
-1

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;
    }
vootla561
источник