Зачем переопределять equals и hashcode одновременно
Перейти к содержимому

Зачем переопределять equals и hashcode одновременно

  • автор:

Кофе-брейк #168. Зачем переопределять методы equals и hashcode в Java?

Зачем переопределять методы equals и hashcode в Java?

Кофе-брейк #168. Зачем переопределять методы equals и hashcode в Java? - 1

Источник: Medium Содержание этой статьи посвящено двум тесно связанным между собой методам: equals() и hashcode() . Вы узнаете, как они взаимодействуют друг с другом и как их правильно переопределять.

Почему мы переопределяем метод equals()?

В Java мы не можем перегружать поведение таких операторов, как == , += , -+ . Они работают согласно заданному процессу. Для примера рассмотрим работу оператора == .

Как работает оператор ==?

Он проверяет, указывают ли две сравниваемые ссылки на один и тот же экземпляр в памяти. Оператор == будет иметь значение true только в том случае, если эти две ссылки представляют один и тот же экземпляр в памяти. Давайте взглянем на пример кода:

 public class Person

Допустим, в вашей программе вы создали два объекта Person в разных местах и ​​хотите их сравнить.

 Person person1 = new Person("Mike", 34); Person person2 = new Person("Mike", 34); System.out.println( person1 == person2 ); --> will print false! 

С точки зрения бизнеса эти два объекта выглядят одинаково, верно? Но для JVM они не совпадают. Поскольку они оба созданы с помощью ключевого слова new , эти экземпляры расположены в разных сегментах памяти. Поэтому оператор == вернет false. Но если мы не можем переопределить оператор == , то как нам сказать JVM, что мы хотим, чтобы эти два объекта обрабатывались одинаково? Здесь в игру вступает метод .equals() . Вы можете переопределить equals() , чтобы проверить, имеют ли некоторые объекты одинаковые значения для определенных полей, чтобы считать их равными. Вы можете выбрать, какие поля нужно сравнить. Если мы говорим, что два объекта Person будут одинаковыми только тогда, когда они имеют одинаковый возраст и одно и то же имя, то в этом случае IDE сгенерирует для автоматического создания equals() что-то такое:

 @Override public boolean equals(Object o)

Вернемся к нашему предыдущему примеру.

 Person person1 = new Person("Mike", 34); Person person2 = new Person("Mike", 34); System.out.println ( person1 == person2 ); --> will print false! System.out.println ( person1.equals(person2) ); --> will print true! 

Да, мы не можем перегрузить оператор == для сравнения объектов так, как мы хотим, но Java дает нам другой способ — метод equals() , который мы можем переопределить по своему усмотрению. Имейте в виду, что если мы не предоставим нашу пользовательскую версию .equals() (также известную как переопределение) в нашем классе, то предопределенный .equals() из класса Object и оператор == будут вести себя одинаково. Метод по умолчанию equals() , унаследованный от Object , будет проверять, совпадают ли оба сравниваемых экземпляра в памяти!

Почему мы переопределяем метод hashCode()?

Кофе-брейк #168. Зачем переопределять методы equals и hashcode в Java? - 2

Некоторые структуры данных в Java, такие как HashSet и HashMap , хранят свои элементы на основе хеш-функции, которая применяется к этим элементам. Хеш-функцией является hashCode() . Если у нас есть выбор в переопределении метода .equals() , то у нас также должен быть выбор в переопределении метода hashCode() . Для этого есть причина. Ведь реализация по умолчанию hashCode() , унаследованная от Object , считает все объекты в памяти уникальными! Но вернемся к этим структурам хеш-данных. Для этих структур данных существует правило. HashSet не может содержать повторяющиеся значения, а HashMap не может содержать повторяющиеся ключи. HashSet реализован с помощью HashMap таким образом, что каждое значение HashSet хранится как ключ в HashMap . Как работает HashMap ? HashMap — это собственный массив с несколькими сегментами. Каждый сегмент имеет связанный список ( linkedList ). В этом связанном списке хранятся наши ключи. HashMap находит правильный linkedList для каждого ключа, применяя метод hashCode() , а затем выполняет итерацию по всем элементам этого linkedList и применяет метод equals() к каждому из этих элементов, чтобы проверить, содержится ли там этот элемент. Дубликаты ключей не допускаются. Когда мы помещаем что-то внутрь HashMap , то ключ сохраняется в одном из этих связанных списков. В каком связанном списке будет храниться этот ключ, показывает результат метода hashCode() для этого ключа. То есть, если key1.hashCode() в результате получается 4, то этот key1 будет храниться в 4-м сегменте массива в существующем там LinkedList . По умолчанию метод hashCode() возвращает разные результаты для каждого экземпляра. Если у нас есть значение по умолчанию equals() , которое ведет себя как == , рассматривая все экземпляры в памяти как разные объекты, то проблем не будет. Как вы помните, в нашем предыдущем примере было сказано, что мы хотим, чтобы экземпляры Person считались равными, если их возраст и имена совпадают.

 Person person1 = new Person("Mike", 34); Person person2 = new Person("Mike", 34); System.out.println ( person1.equals(person2) ); --> will print true! 

Теперь давайте создадим карту (map) для хранения этих экземпляров в виде ключей с определенной строкой в ​​качестве парного значения.

 Map map = new HashMap(); map.put(person1, "1"); map.put(person2, "2"); 

Кофе-брейк #168. Зачем переопределять методы equals и hashcode в Java? - 3

В классе Person мы не переопределили метод hashCode , но у нас есть переопределенный метод equals . Поскольку значение по умолчанию hashCode дает разные результаты для разных Java-экземпляров person1.hashCode() и person2.hashCode() , есть большие шансы получить разные результаты. Наша карта может заканчиваться разными person в разных связанных списках. Это противоречит логике HashMap . Ведь HashMap не может иметь несколько одинаковых ключей! Дело в том, что по умолчанию hashCode() , унаследованного от класса Object , недостаточно. Даже после того, как мы переопределили метод equals() класса Person . Вот почему мы должны переопределить метод hashCode() после того, как мы переопределили метод equals . Теперь давайте это исправим. Нам нужно переопределить наш метод hashCode() , чтобы он учитывал те же поля, что и equals() , а именно age и name .

 public class Person < private Integer age; private String name; ..getters, setters, constructors @Override public boolean equals(Object o) < if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age && name.equals(person.name); >@Override public int hashCode()

В методе hashCode() мы использовали простое значение (вы можете использовать любые другие значения). Тем не менее, предлагается использовать простые числа, чтобы создавать меньше проблем. Давайте попробуем еще раз сохранить эти ключи в нашем HashMap :

 Map map = new HashMap(); map.put(person1, "1"); map.put(person2, "2"); 

Кофе-брейк #168. Зачем переопределять методы equals и hashcode в Java? - 4

person1.hashCode() и person2.hashCode() будут одинаковы. Допустим, равны 0. HashMap перейдет в сегмент 0 и в нем LinkedList сохранит person1 как ключ со значением “1”. Во втором случае, когда HashMap снова перейдет к корзине 0, чтобы сохранить ключ person2 со значением “2”, он увидит, что там уже существует другой равный ему ключ. Таким образом он перезапишет предыдущий ключ. И в нашем HashMap будет существовать только ключ person2 . Так мы узнали, как работает правило HashMap , которое гласит, что нельзя использовать несколько одинаковых ключей! Однако имейте в виду, что неравные экземпляры могут иметь одинаковый хэшкод, а одинаковые экземпляры должны возвращать одинаковый хэшкод.

Зачем переопределять методы Equals() и GetHashCode() в C#

Можете объяснить, зачем в коде переопределять данные методы в коде. И при этом почему то пишут что они должны переопределяться вместе. Вот даже просто взял пример. зачем тут это, почему без этого переопределения не работает. Как оно все устроено.

Person[] students = < new Person("Tom"), new Person("Bob"), new Person("Sam") >; Person[] employees = < new Person("Tom"), new Person("Bob"), new Person("Mike") >; // объединение последовательностей var people = students.Union(employees); foreach (Person person in people) Console.WriteLine(person.Name); class Person < public string Name < get;>public Person(string name) => Name = name; public override bool Equals(object? obj) < if (obj is Person person) return Name == person.Name; return false; >public override int GetHashCode() => Name.GetHashCode(); > 

Отслеживать
задан 23 окт 2022 в 10:36
1 1 1 бронзовый знак
А в книге по C# этот момент не объясняется?
23 окт 2022 в 10:42
23 окт 2022 в 10:55

Ну, что-то будет работать. Но как только вы захотите использовать ваши объекты в словаре в качестве ключей или в хэшсете, тут то всё это и понадобится. Ну и если вы объекты просто где-то захотите сравнивать, то Equals само по себе понадобится.

23 окт 2022 в 11:01

Автор, а вы представьте себя на месте метода Union. Вот вам дали на вход два набора объектов, и вам надо выдать на выход один набор без повторов. Как вы будете решать эту задачу без возможности сравнить объекты?

23 окт 2022 в 11:16

2 ответа 2

Сортировка: Сброс на вариант по умолчанию

Equals переопределяется, чтобы сравнивать объекты. К примеру, у тебя 2 объекта одного типа, и они могут быть равны или одинаковы по каким-то условиям, общая сумма всех внутренних переменных одинакова или полностью идентичны, но быть независимыми друг от друга. Для этого и существует метод Equals, который для других пользователей твоего куска кода, позволяет сравнивать объекты. Можно так-же этому методы дать перегрузки, чтоб он принимал не только «object? obj», но и «int» и прочие типы объектов, но там уже переопределений не будет.

GetHashCode используется для ускорения сравнения двух объектов. То есть если требуется узнать, одинаковы ли какие-то два объекта, то сначала сравниваются их хэш-коды. Если они различаются, то значит и объекты различны. Если же совпадают, то тогда начинается дорогостоящее «настоящее» сравнение через Equals. Таким образом, GetHashCode, во-первых, должен совпадать для одинаковых объектов, во-вторых, по-возможности отличаться для разных объектов и в-третьих, достаточно быстро вычисляться (чтобы в его использовании вообще был смысл). В вашем случае вряд ли преобразование целых в строку и затем их сравнение будет выполняться быстрее чем сравнение чисел напрямую.

Отслеживать
ответ дан 23 окт 2022 в 11:07
47 6 6 бронзовых знаков

В том-то и дело, что может не объясняться. Часто базовые вещи опускают. Для «GetHashCode()» в документации https://learn.microsoft.com/en-us/dotnet/api/system.object.gethashcode?view=net-6.0 написано: «A hash code is intended for efficient insertion and lookup in collections that are based on a hash table». Это значит, что если использовать класс в качестве ключа коллекции не планируется, то можно и не переопределять. В качестве ключа чаще всего используются какие-то базовые типы вроде int или string, для которых вычисление хеша уже реализовано. «Equals()» используется в паре с «GetHashCode()».

То же самое для «==». Метод позволяет сравнивать объекты с помощью операции равенства. Если такие сравнения не планируются, то можно не переопределять. (Сделал уточнение согласно комментарию.)

Интересно, что переопределение «==» требует переопределения «Equals()», но не наоборот.

Отслеживать
ответ дан 23 окт 2022 в 11:09
4,058 2 2 золотых знака 3 3 серебряных знака 19 19 бронзовых знаков

== не будет работать, если вы просто переопределите эти методы. Для их работы нужно переопределять сами операторы дополнительно.

Перегрузка методов equals() и hashCode() в Java

Переопределение методов equals() и hashCode() в Java

Перегрузка методов equals() и hashCode() в Java - 1

Equals и hashCode являются фундаментальными методами объявленные в классе Object и содержатся в стандартных библиотеках Java. Метод еquals() используется для сравнения объектов, а hashCode — для генерации целочисленного кода объекта. Эти методы широко используются в стандартных библиотеках Java при вставке и извлечении объектов в HashMap . Метод equal также используется для обеспечения хранения только уникальных объектов в HashSet и других Set реализациях, а также в любых других случаях, когда нужно сравнивать объекты. Реализация по умолчанию метода equals() в классе java.lang.Object сравнивает ссылки на адреса в памяти, которые хранят переменные, и возвращает true только в том случае, если адреса совпадают, другими словами переменные ссылаются на один и тот же объект. Java рекомендует переопределять методы equals() и hashCode() , если предполагается, что сравнение должно осуществляться в соответсвии с естественной логикой или бизнес-логикой. Многие классы в стандартных библиотеках Java переопределяют их, например в классе String переопределяется equals таким образом, что возвращается true , если содержимое двух сравниваемых объектов одинаковое. В классе-обертке Integer метод equal переопределяется для выполнения численного сравнения, и так далее. Так как HashMap и HashTable в Java полагаются на методы equals() и hashCode() для сравнения своих key и values , то Java предлагает следующие правила для переопределения этих методов:

  1. Рефлексивность: Объект должен равняться себе самому.
  2. Симметричность: если a.equals(b) возвращает true , то b.equals(a) должен тоже вернуть true .
  3. Транзитивность: если a.equals(b) возвращает true и b.equals(c) тоже возвращает true , то c.equals(a) тоже должен возвращать true .
  4. Согласованность: повторный вызов метода equals() должен возвращать одно и тоже значение до тех пор, пока какое-либо значение свойств объекта не будет изменено. То есть, если два объекта равны в Java, то они будут равны пока их свойства остаются неизменными.
  5. Сравнение null : объект должны быть проверен на null . Если объект равен null , то метод должен вернуть false , а не NullPointerException . Например, a.equals(null) должен вернуть false .

Соглашение между equals и hashCode в Java

  1. Если объекты равны по результатам выполнения метода equals , тогда их hashcode должны быть одинаковыми.
  2. Если объекты не равны по результатам выполнения метода equals , тогда их hashcode могут быть как одинаковыми, так и разными. Однако для повышения производительности, лучше, чтобы разные объекты возвращали разные коды.

Как переопределять метод equals в Java

 @Override public boolean equals(Object obj) < /*1. Проверьте*/if (obj == this) < /*и верните */ return true; > 
 if (obj == null || obj.getClass() != this.getClass())
 Person guest = (Person) obj; return guest.id && (firstName == guest.firstName || (firstName != null && firstName.equals(guest.getFirstName()))) && (lastName == guest.lastName || (lastName != null && lastName .equals(guest.getLastName()))); > 
 /** * Person class with equals and hashcode implementation in Java * @author Javin Paul */ public class Person < private int id; private String firstName; private String lastName; public int getId() < return id; >public void setId(int id) < this.id = id;>public String getFirstName() < return firstName; >public void setFirstName(String firstName) < this.firstName = firstName; >public String getLastName() < return lastName; >public void setLastName(String lastName) < this.lastName = lastName; >@Override public boolean equals(Object obj) < if (obj == this) < return true; >if (obj == null || obj.getClass() != this.getClass()) < return false; >Person guest = (Person) obj; return guest.id && (firstName == guest.firstName || (firstName != null &&firstName.equals(guest.getFirstName()))) && (lastName == guest.lastName || (lastName != null && lastName .equals(guest.getLastName()) )); > @Override public int hashCode() < final int prime = 31; int result = 1; result = prime * result + ((firstName == null) ? 0 : firstName.hashCode()); result = prime * result + id; result = prime * result + ((lastName == null) ? 0 : lastName.hashCode()); return result; >> 

Распространенные ошибки при переопределении equals в Java

  1. Вместо того, чтобы переопределять метод equals (Override) программист перегружает его (Overload) Синтаксис метода equals() в классе Object определен как public boolean equals(Object obj) , но многие программисты ненароком перегружают метод: public boolean equals(Person obj) — вместо Object в качестве аргумента используют имя своего класса (напр. Person). Эту ошибку сложно обнаружить из-за static binding . Таким образом, если вы вызовете этот метод для объекта своего класса, то метод не просто скомпилируется, а даже сделает это корректно. Однако, если вы положите ваш объект в коллекцию, например ArrayList и вызовете метод contains() , работа которого основана на методе equals() , то метод contains не сможет обнаружить ваш объект.
  2. При переопределении метода equals() не проверять на null переменные, что в конечном итоге заканчивается NullPointerException при вызове equals() . Ниже представлен корректный код.
 firstname == guest.firstname || (firstname != null && firstname.equals(guest.firstname)); 

Подсказки как писать в Java метод equals

  1. Большинство IDE такие как NetBeans, Eclipse и IntelliJ IDEA обеспечивают поддержку генерации методов equals() и hashCode() . В Eclipse нажмите правую кнопку -> source -> generate equals() и hashCode() .
  2. Если в классе есть уникальный бизнес-ключ, то будет достаточно сделать проверку только на равенство этих полей. Как в нашем примере “id” — уникальный номер для каждого Person.
  3. При переопределении hashCode() в Java удостоверьтесь в использовании всех полей, что были использованы в методе equals() .
  4. String и классы-оболочки такие как Integer , Float и Double переопределяют метод equals() , но StringBuffer не переопределяет.
  5. При любой возможности делайте поля immutable используя final переменные в Java.
  6. При сравнении String объектов используйте equals() вместо оператора == .
  7. Два объекта которые логически равны, но загружены из разных ClassLoader не могут быть равными. Помните, что проверка с помощью getClass() вернет false если класс-загрузчик разный.
  8. Используйте @Override аннотацию также для метода hashCode , так как это предупреждает неуловимые ошибки, например возвращаемое значение метода int , однако некоторые программисты возвращают long .

Методы .equals и .hashcode в Java. Отличия реализации по умолчанию от реализации на практике

Сразу же скажу, что статья во многом опирается базовые понятия алгебры, которые к великому счастью легко и быстро осознаются при помощи всего-лишь метода внимательного разглядывания. Поехали.

В Java так устроено, что любой класс, который вы определяете, наследуется от класса Object. Таким образом класс Object является суперклассом любого класса в любой программе.

Это означает, что абсолютно любой класс содержит методы, которые определены в классе Object. Методы .equals() и .hashcode() — одни из них.

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

1). Если x.equals(y) == true, то обязательно hashcode(x) == hashcode(y)

2) Если hashcode(x) == hashcode(y), то не обязательно x.equals(y) == true

Метод .equals()

Отношение эквивалентности (алгебра)

Прежде чем поговорить о методе .equals, я бы хотел рассказать, что такое отношение эквивалентности с точки зрения алгебры (пока-что забудьте про программирование).

Отношение эквивалентности — это бинарное (бинарное — значит между двумя) отношение, которое является:

Таким образом, если на множестве определено отношение эквивалентности, множество можно разделить на подмножества — классы эквивалентности.

Каждый класс эквивалентности содержит внутри себя только те элементы, которые эквиваленты (более формально — находятся в отношении эквивалентности) между собой.

Реализация .equals() по умолчанию

Метод .equals() в классе Object реализован примерно следующим образом:

public boolean equals(Object x)

Фактически он делает следующее: Он принимает в качестве аргумента ссылочную переменную и проверяет, ссылается ли они на тот же объект (ту же область памяти, если быть точнее), что и объект, к которому мы применили метод .equals().

Таким образом, стандартная реализация .equals() выстраивает отношение эквивалентности, которое можно описать так: две ссылки эквивалентны, если они ссылаются на одну и ту же область памяти.

Такая реализация не противоречит математической идеологии, описанной выше. Однако на практике метод .equals() часто переопределяют в подклассах.

Как и зачем переопределяют метод .equals()?

Очевидно, гораздо более применимой будет возможность сравнивать объекты по какому-нибудь другому критерию. Часто метод .equals() переопределяют так, чтобы он сравнивал объекты по значениям их полей.

К примеру, если классы двух объектов, на которые указывают ссылки, совпадают и все значения их полей совпадают, то эти два объекта эквивалентны между собой. Легко проследить, что такое определение не противоречит математической идеологии.

Конкретную кодовую реализацию я приводить не буду, потому что она не так важна, как сама идея

Это и другие возможные переопределения метода .equals() мало того, что расширяют круг наших возможностей, так ещё и не лишают старых, ведь мы по прежнему имеем возможность проверять, ссылаются ли две ссылки на одну область памяти, используя операнд ==, вместо прежнего .equals()

return(ob1 == ob2);

Метод .hashcode()

Сюръекция (алгебра)

Сюръекция — сопоставление элементам множества X элементов второго множества Y, при котором для любого элемента из Y есть хотя-бы один сопоставленный элемент из X.

Если немного более подробно разобрать это определение, то мы увидим следующее:

  • Даже несколько элементов из X могут быть сопоставлены одному и тому же элементу из Y (это называется коллизией).
  • Возможно есть такое элемент из X, и даже возможно не один, что он не сопоставлен никакому элементу из Y. (см. рисунок, всё интуитивно)

Что происходит в java?

Метод .hashcode() как-раз осуществляет сюръекцию. Множеством X выступает множество всевозможных объектов которые мы можем создать, множеством Y выступает область значений типа данных int. Метод .hashcode() вычисляет каким-то скрытым от нас способом целое число, опираясь на объект, к которому применяется.

Единственное отличие метода .hashcode() от сюръекции в том, что любой объект может быть обработан методом .hashcode()

Здесь нет элементов по типу E из пред. рисунка

Реализация .hashcode() по умолчанию?

Насколько я понял, точно так никто в этом и не разобрался. Есть много версий:

  • Значение .hashcode() — это область памяти, где лежит объект
  • Значение .hashcode() — это число, создаваемое генератором случайных чисел в какой-то момент
  • Сама функция написана не на Java а вообще на C.

И многие другие. В общем каким-то образом она всё же устроена, но самое главное в том, что стандартная реализация .hashcode() со стандартной реализацией .equals() подчиняются правилу, приведённому в самом начале статьи

Как и зачем переопределяют метод .hashcode()?

Основной причиной для изменения метода .hashcode() является то, что желают изменить .equals(), однако смена стандартной реализации .equals() приводит к нарушению правила из начала статьи

Второстепенной причиной для изменения метода .hashcode() является то, что желают изменить вероятность коллизии (эта причина встречается реже)

Конец 🙂

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *