Какие проблемы следует учитывать при переопределении equals и hashCode в Java?
Проблема с переопределением методов equals и hashCode в Java
Я столкнулся с вопросом, связанным с переопределением методов equals и hashCode в Java. В частности, хотел бы понять, какие подводные камни и проблемы могут возникнуть в процессе их переопределения.
Согласованность: Как обеспечить, чтобы
equalsиhashCodeвсегда возвращали согласованные значения? Какие аспекты следует учитывать при переопределении этих методов для различных типов данных?Контракт методов: Какие важные моменты нужно учитывать для соблюдения контракта между
equalsиhashCode? Как избежать ситуаций, когда один из методов нарушает условия, предписанные спецификацией?Производительность: Как производительность влияла на выбор алгоритмов для этих методов? Есть ли рекомендации по оптимизации кода, чтобы избежать потенциальных узких мест?
Изменяемость: Стоит ли переопределять эти методы для изменяемых объектов? Какие проблемы могут возникнуть, если объекты изменяются после того, как они были добавлены в коллекции, такие как
HashMapилиHashSet?
Буду признателен за советы и примеры, которые помогут лучше разобраться в этом вопросе!
5 ответ(ов)
При использовании классов, которые сохраняются с использованием объектно-реляционного отображения (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().
Зачем стоит учитывать условие 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().
Никто не упомянул библиотеку 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 может значительно улучшить читаемость и краткость кода!
Чтобы переопределить методы 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;
}
// другие методы
}
Ключевые моменты:
- Метод
equalsсначала сравнивает ссылки (если это один и тот же объект), затем проверяет, является ли объект null или относится ли к другому классу. - Для сравнения строк в
equalsиспользуетсяequals, что позволяет корректно обрабатывать null. - Метод
hashCodeвычисляет хеш-код, основываясь на полях класса. Еслиdataравно null, мы используем 0 в качестве хеш-кода.
Это поможет вам обеспечить правильное поведение при использовании объектов вашего класса в хеш-таблицах, например, при использовании их в HashMap или HashSet.
Если вам нужно больше информации, посмотрите по следующей ссылке: JavaRanch и Java67.
Удачи! @.@
Существует несколько способов проверки равенства классов перед проверкой равенства членов, и каждый из них полезен в определенных обстоятельствах.
- Использовать оператор
instanceof. - Использовать
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;
}
}
Ошибка «Необходимо переопределить метод суперкласса» после импорта проекта в Eclipse
Почему в Java нельзя переопределять статические методы?
Как вызвать метод суперкласса с использованием рефлексии в Java
Как объявить массив в одну строку?
Как преобразовать jsonString в JSONObject в Java?