Какие проблемы следует учитывать при переопределении 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 нельзя переопределять статические методы?
Как объявить массив в одну строку?
Загрузка JDK Java на Linux через wget приводит к отображению страницы лицензии вместо установки
Создание репозитория Spring без сущности