6

Какие проблемы следует учитывать при переопределении equals и hashCode в Java?

14

Проблема с переопределением методов equals и hashCode в Java

Я столкнулся с вопросом, связанным с переопределением методов equals и hashCode в Java. В частности, хотел бы понять, какие подводные камни и проблемы могут возникнуть в процессе их переопределения.

  1. Согласованность: Как обеспечить, чтобы equals и hashCode всегда возвращали согласованные значения? Какие аспекты следует учитывать при переопределении этих методов для различных типов данных?

  2. Контракт методов: Какие важные моменты нужно учитывать для соблюдения контракта между equals и hashCode? Как избежать ситуаций, когда один из методов нарушает условия, предписанные спецификацией?

  3. Производительность: Как производительность влияла на выбор алгоритмов для этих методов? Есть ли рекомендации по оптимизации кода, чтобы избежать потенциальных узких мест?

  4. Изменяемость: Стоит ли переопределять эти методы для изменяемых объектов? Какие проблемы могут возникнуть, если объекты изменяются после того, как они были добавлены в коллекции, такие как HashMap или HashSet?

Буду признателен за советы и примеры, которые помогут лучше разобраться в этом вопросе!

5 ответ(ов)

3

При использовании классов, которые сохраняются с использованием объектно-реляционного отображения (ORM), такого как Hibernate, стоит обратить внимание на некоторые нюансы, если вы думали, что это не слишком усложнено!

Ленивая загрузка объектов — это подклассы

Когда ваши объекты сохраняются с помощью ORM, в большинстве случаев вы будете иметь дело с динамическими прокси, чтобы избежать предварительной загрузки объектов из хранилища данных. Эти прокси реализованы как подклассы вашего собственного класса. Это означает, что this.getClass() == o.getClass() вернет false. Например:

Person saved = new Person("John Doe");
Long key = dao.save(saved);
dao.flush();
Person retrieved = dao.retrieve(key);
saved.getClass().equals(retrieved.getClass()); // Вернет false, если Person загружен лениво

Если вы работаете с ORM, использование o instanceof Person — это единственный способ, который будет работать корректно.

Ленивая загрузка объектов и null-поля

ORM обычно использует геттеры для заставления загружать лениво загруженные объекты. Это означает, что person.name будет равно null, если person загружен лениво, даже если person.getName() заставляет загрузить данные и возвращает "John Doe". На моем опыте, это чаще всего проявляется в методах hashCode() и equals().

Если вы работаете с ORM, всегда используйте геттеры и никогда не обращайтесь к полям напрямую в hashCode() и equals().

Сохранение объекта изменит его состояние

Постоянные объекты обычно используют поле id для хранения ключа объекта. Это поле будет автоматически обновлено при первом сохранении объекта. Не используйте поле id в hashCode(). Но вы можете использовать его в equals().

Шаблон, который я часто использую, выглядит так:

if (this.getId() == null) {
    return this == other;
} else {
    return this.getId().equals(other.getId());
}

Но: вы не можете включать getId() в hashCode(). Если вы это сделаете, при сохранении объекта его hashCode изменится. Если объект находится в HashSet, вы "никогда" не найдете его снова.

В моем примере с Person я, вероятно, использовал бы getName() для hashCode и getId() плюс getName() (просто на всякий случай) для equals(). Наличие определенных рисков "коллизий" для hashCode() допустимо, но никогда не допустимо для equals().

hashCode() должен использовать неизменяемую подмножество свойств из equals().

0

Зачем стоит учитывать условие obj.getClass() != getClass() при реализации метода equals()?

Это утверждение напрямую связано с тем, что equals() не является дружелюбным к наследованию. Спецификация языка Java (JLS) указывает, что если A.equals(B) == true, то также должно выполняться B.equals(A) == true. Если это условие опустить, то наследующие классы, переопределяющие equals() и изменяющие его поведение, могут нарушить данную спецификацию.

Рассмотрим пример, что происходит, если это условие пропустить:

class A {
    int field1;

    A(int field1) {
        this.field1 = field1;
    }

    public boolean equals(Object other) {
        return (other != null && other instanceof A && ((A) other).field1 == field1);
    }
}

class B extends A {
    int field2;

    B(int field1, int field2) {
        super(field1);
        this.field2 = field2;
    }

    public boolean equals(Object other) {
        return (other != null && other instanceof B && ((B) other).field2 == field2 && super.equals(other));
    }
}

При вызове new A(1).equals(new A(1)) и new B(1,1).equals(new B(1,1)) оба возвращают true, как и ожидалось.

Однако, давайте посмотрим, что произойдет при использовании обоих классов вместе:

A a = new A(1);
B b = new B(1, 1);
a.equals(b) == true; // Логически неверно
b.equals(a) == false; // Логически неверно

Явно видно, что это неправильно.

Чтобы гарантировать симметрию, то есть, чтобы a.equals(b) возвращал то же самое, что и b.equals(a), а также соблюдать принцип подстановки Лисков, необходимо вызвать super.equals(other) не только в случае экземпляра B, но и проверить, является ли other экземпляром A:

if (other instanceof B) 
    return (other != null && ((B) other).field2 == field2 && super.equals(other)); 
if (other instanceof A) 
    return super.equals(other); 
else 
    return false;

Такой подход позволит получить:

a.equals(b) == true; 
b.equals(a) == true;

Таким образом, если a не является ссылкой на B, то, скорее всего, это ссылка на класс A (поскольку вы его расширяете). В этом случае вам также нужно вызывать super.equals().

0

Никто не упомянул библиотеку Guava для этого? Это действительно удивляет! Она предоставляет удобные утилиты для работы с hash-кодами и проверкой на равенство. Вот как можно переписать ваш код с использованием методов из Guava:

import com.google.common.base.Objects;

// Пример использования из проекта

@Override
public int hashCode(){
    return Objects.hashCode(this.getDate(), this.datePattern);
}

@Override
public boolean equals(Object obj){
    if (!(obj instanceof DateAndPattern)) {
        return false;
    }
    DateAndPattern other = (DateAndPattern) obj;
    return Objects.equal(this.getDate(), other.getDate())
            && Objects.equal(this.datePattern, other.datePattern);
}

Обратите внимание, что в equals я добавил сравнение this.datePattern с other.datePattern, чтобы гарантировать, что оба поля действительно сравниваются. Использование Guava может значительно улучшить читаемость и краткость кода!

0

Чтобы переопределить методы equals и hashCode в вашем классе, вы можете использовать следующий код. Это обеспечит правильное сравнение объектов и вычисление хеш-кодов:

public class Test {
    private int num;
    private String data;

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true; // Сравниваем ссылки
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false; // Проверяем на null и классы
        }
        // Здесь мы уверены, что объект является экземпляром Test
        Test test = (Test) obj;
        return num == test.num && 
               (data == test.data || (data != null && data.equals(test.data)));
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 31 * hash + num; // Вычисляем хеш на основе num
        hash = 31 * hash + (data == null ? 0 : data.hashCode()); // Обработка null для data
        return hash;
    }

    // другие методы
}

Ключевые моменты:

  1. Метод equals сначала сравнивает ссылки (если это один и тот же объект), затем проверяет, является ли объект null или относится ли к другому классу.
  2. Для сравнения строк в equals используется equals, что позволяет корректно обрабатывать null.
  3. Метод hashCode вычисляет хеш-код, основываясь на полях класса. Если data равно null, мы используем 0 в качестве хеш-кода.

Это поможет вам обеспечить правильное поведение при использовании объектов вашего класса в хеш-таблицах, например, при использовании их в HashMap или HashSet.

Если вам нужно больше информации, посмотрите по следующей ссылке: JavaRanch и Java67.

Удачи! @.@

0

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

  1. Использовать оператор instanceof.
  2. Использовать this.getClass().equals(that.getClass()).

Я предпочитаю вариант #1 в final реализации метода equals, или когда реализую интерфейс, который задаёт алгоритм для equals (например, интерфейсы коллекций из java.util — правильный способ проверки с помощью (obj instanceof Set) или того интерфейса, который вы реализуете). Обычно это не лучший выбор, когда equals может быть переопределён, поскольку это нарушает свойство симметрии.

Вариант #2 позволяет классу безопасно расширяться без необходимости переопределять equals и нарушать симметрию.

Если ваш класс также реализует Comparable, методы equals и compareTo также должны быть согласованы. Вот шаблон для метода equals в классе, реализующем Comparable:

final class MyClass implements Comparable<MyClass>
{

  …

  @Override
  public boolean equals(Object obj)
  {
    /* Если compareTo и equals не являются final, следует проверить с помощью getClass вместо этого. */
    if (!(obj instanceof MyClass)) 
      return false;
    return compareTo((MyClass) obj) == 0;
  }

}
Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь