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

от admin

Java Challengers #4: Сравнение объектов с equals() и hashCode()

В этой статье вы узнаете, как связаны между собой методы equals() и hashCode() и как они используются при сравнении объектов.

Без использования equals() и hashCode() для сравнения состояния двух объектов нам нужно писать много сравнений » if «, сравнивая каждое поле объекта. Такой подход делает код запутанным и трудным для чтения. Работая вместе, эти два метода помогают создавать более гибкий и согласованный код.

Исходный код для статьи находится здесь.

Переопределение equals() и hashCode()

Переопределение метода (method overriding) — это приём при котором поведение родительского класса или интерфейса переписывается (переопределяется) в подклассе (см. Java Challengers #3: Полиморфизм и наследование, анг.). В Java у каждого объекта есть методы equals() и hashCode() и для правильной работы они должны быть переопределены.

Чтобы понять, как работает переопределение equals() и hashCode() , изучим их реализацию в базовых классах Java. Ниже приведён метод equals() класса Object . Метод проверяет, совпадает ли текущий экземпляр с переданным объектом obj .

Теперь посмотрим на метод hashCode() в классе Object .

Это native — метод, который написан на другом языке, таком как Си, и он возвращает некоторый числовой код, связанный с адресом памяти объекта. (Если вы не пишете код JDK, то не важно точно знать, как работает этот метод.)
Примечание переводчика: про значение, связанное с адресом сказано не совсем корректно (спасибо vladimir_dolzhenko). В HotSpot JVM по умолчанию используются псевдослучайные числа. Описание реализации hashCode() для HotSpot, есть здесь и здесь.

Если методы equals() и hashCode() не переопределены, вместо них будут вызваны методы класса Object , описанные выше. В этом случае методы не выполняют реальной цели equals() и hashCode() , которая состоит в том, чтобы проверить, имеют ли объекты одинаковые состояния.

Как правило, при переопределении equals() также переопределяется hashCode() .

Сравнение объектов с equals()

Метод equals() используется для сравнения объектов. Чтобы определить одинаковые объекты или нет, equals() сравнивает значения полей объектов:

Посмотрим на метод equals() . Первое сравнение сравнивает текущий экземпляр объекта this с переданным объектом o . Если это один и тот же объект, то equals() вернёт true .

Во втором сравнении проверяется, является ли переданный объект null и какой у него тип. Если переданный объект другого типа, то объекты не равны.

Наконец, equals() сравнивает поля объектов. Если два объекта имеют одинаковые значения полей, то объекты совпадают.

Анализ вариантов сравнения объектов

Теперь давайте посмотрим на варианты сравнений объектов в методе main() . Сначала мы сравниваем два объекта Simpson :

У полей этих объектов одинаковые значения, поэтому результат будет true .

Затем снова сравниваем два объекта Simpson :

Объекты здесь похожи, но значения имён разные: Bart и El Barto. Поэтому результат будет false .

Наконец, давайте сравним объект Simpson и экземпляр класса Object :

В этом случае результат будет false , так как типы объектов отличаются.

equals() в сравнении с ==

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

Мы создали два разных экземпляра Simpson с помощью оператора new . Поэтому переменные homer и homer2 будут указывать на разные объекты в куче. Таким образом, в результате получим false .

Во следующем примере используем переопределенный метод equals() :

В этом случае будут сравниваться поля. Поскольку значения полей у обоих объектов Simpson одинаковые, результат будет true .

Идентификация объектов с hashCode()

Для оптимизации производительности при сравнении объектов используется метод hashCode() . Метод hashCode() возвращает уникальный идентификатор для каждого объекта, что упрощает сравнение состояний объектов.

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

Рассмотрим практический пример с hashCode() .

Метод hashCode() , который всегда возвращает одно и то же значение, допустим, но не эффективен. В этом случае сравнение всегда будет возвращать true , поэтому метод equals() будет выполняться всегда. В этом случае нет никакого улучшения производительности.

Использование equals() и hashCode() с коллекциями

Классы, реализующие интерфейс Set (множество) должны не допускать добавления повторяющихся элементов. Ниже приведены некоторые классы, реализующие интерфейс Set :

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

Посмотрим на часть реализации метода add() в HashSet :

Перед добавлением нового элемента HashSet проверяет, существует ли элемент в данной коллекции. Если объект совпадает, то новый элемент вставляться не будет.

Методы equals() и hashCode() используются не только в Set . Также эти методы требуются для HashMap, Hashtable, и LinkedHashMap. Как правило, если вы видите коллекцию с префиксом «Hash», вы можете быть уверены, что для её корректной работы требуется переопределение методов hashCode() и equals() .

Рекомендации по использованию equals() и hashCode()

Выполняйте метод equals() только для объектов с одинаковым хэш-кодом. Не выполняйте equals() , если хэш-код отличается.

Таблица 1. Сравнение хэш-кодов

Если сравнение hashCode() . То .
возвращает true выполнить equals()
возвращает false не выполнять equals()

Этот принцип в основном используется в коллекциях Set или Hash по соображениям производительности.

Правила сравнения объектов

Когда сравнение hashCode() возвращает false , метод equals() также должен возвращать false . Если хэш-код отличается, то объекты определенно не равны.

Таблица 2.Сравнение объектов с hashCode()

Когда сравнение hashCode() возвращает . Метод equals() должен вернуть .
true true или false
false false

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

Таблица 3. Сравнение объектов с equals()

Когда метод equals() возвращает . Метод hashCode() должен вернуть .
true true
false true или false

Решите задачку на equals() и hashCode()

Пришло время проверить ваши знания методов equals() и hashCode() . Задача состоит в том, чтобы выяснить результат нескольких equals() и итоговый размер коллекции Set .

Для начала, внимательно изучите следующий код :

Сначала проанализируйте код, подумайте, какой будет результат. И только потом запустите код. Цель в том, чтобы улучшить ваши навыки анализа кода и усвоить основные концепции Java, чтобы вы могли сделать свой код лучше.

Какой будет результат?.

Что произошло? Понимание equals() и hashCode()

В первом сравнении результат equals() равен true , поскольку состояния объектов одинаковые, и метод hashCode() возвращает одно и то же значнеие для обоих объектов.

Во втором сравнении для переменной overriddenHomer был переопределён метод hashCode() . Для обоих объектов Simpson имя равно «Homer», но для overriddenHomer метод hashCode() возвращает другое значение. В этом случае результат метода equals() будет false , так как в нём содержится сравнение с хэш-кодом.

Вы, должно быть, поняли, что в коллекции будет три объекта Simpson . Давайте разберём это.

Первый объект в наборе будет вставлен как обычно:

Следующий объект также будет вставлен в обычном порядке, поскольку содержит значение, отличное от предыдущего объекта:

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

Как мы знаем, объект overridenHomer использует другое значение хэш-кода в отличие от обычного экземпляра Simpson(«Homer») . По этой причине этот элемент будет вставлен в коллекцию:

Ответ

Правильный ответ — B. Вывод будет:

Частые ошибки с equals() и hashCode()

  • Отсутствие переопределения hashCode() вместе с переопределением equals() или наоборот.
  • Отсутствие переопределения equals() и hashCode() при использовании хэш-коллекций, таких как HashSet .
  • Возврат постоянного значения в методе hashCode() вместо возврата уникального кода для каждого объекта.
  • Равнозначное использование == и equals() . Оператор == сравнивает ссылки на объекты, тогда как метод equals() сравнивает значения объектов.

Что нужно помнить о equals() и hashCode()

  • Рекомендуется всегда переопределять методы equals() and hashCode() в ваших POJO (рус., анг.)
  • Используйте эффективный алгоритм для создания уникального хэш-кода.
  • При переопределении метода equals() всегда переопределяйте метод hashCode() .
  • Метод equals() должен сравнивать полное состояние объектов (значения из полей).
  • Метод hashCode() может быть идентификатором (ID) POJO.
  • Если результат сравнения хэш-кода двух объектов false , то метод equals() также должен иметь значение false .
  • Если equals() и hashCode() не переопределяются при использовании хэш-коллекций, то коллекция будет иметь повторяющиеся элементы.

Изучите больше о Java

  • Посмотрите больше примеров работы с equals() и hashCode() (анг.).
  • См. Java Dev Gym и NoBugsProject от Рафаэля Дел Неро (Rafael Del Nero).

Традиционно жду ваши комментарии и приглашаю на открытый урок, который уже 18 марта проведет наш преподаватель Сергей Петрелевич

Name already in use

JBook / object / hashcode.md

  • Go to file T
  • Go to line L
  • Copy path
  • Copy permalink

2 contributors

Users who have contributed to this file

  • Open with Desktop
  • View raw
  • Copy raw contents Copy raw contents

Copy raw contents

Copy raw contents

Что такое hash ? Это некоторое число, генерируемое на основе объекта и описывающее его состояние. Это число вычисляется на основе hash -функции.
Метод int hashCode() является реализация hash -функции для Java -объекта. Возвращаемое число в Java считается hash -кодом объекта.

Объявление метода выглядит так:

Возможно, вас напугала стрчока J9VMInternals.fastIdentityHashCode(this), но пока для простоты считайте, что эта реализация зависит от JVM .

В hash — таблицах или ассоциативных маассивах (их еще называют словари, мапы) важность hashCode -а нельзя недооценивать.

Представим, что у нас есть массив пар ключ-значение. Для поиска по ключу в таком массиве вам надо обойти весь массив, заглянуть в каждую ячейку и сравнить ключ. Долго? Долго!

А теперь вспомним, что хэш — это по сути метаинформация о состоянии объекта, некоторое число. И мы можем просто взять хэш, по нему понять в какой ячейке хранится искомая пара и просто взять ее оттуда, без перебора всего массива! Быстро? Гораздо быстрее, чем просто итерация по всему массиву!

Да, могут быть коллизии — ситуации, когда разным объектам будет сгенерирован одинаковый хэш, но решить коллизию можно через equals и это все еще быстрее, чем проходить весь массив.

По сути, главное преимущество в быстродействии ассоциативных массивов основано на работе с hashCode . Именно за счет хэша мы можем вставлять и получать данные за O(1) , то есть за время пропорциональное вычислению хэш-функции.

Если планируется использовать объекты в качестве ключа в ассоциативном массиве, то переопределять hashCode надо обязательно.

Еще раз посмотрим на метод:

Контракт hashCode предъявляет следующие требования к реализации:

  • Если вызвать метод hashCode на одном и том же объекте, состояние которого не меняли, то будет возвращено одно и то же значение.
  • Если два объекта равны, то вызов hashCode для каждого обязан давать один и тот же результат.

Равенство объектов проверяется через вызов метода equals.

Проще говоря, разный hash -код у двух объектов — это гарантия того, что объекты не равны, в то время как одинаковый hash -код не гарантирует равенства.

Cитуация, когда разные объекты имеют одинаковые hash -код, называется collision или коллизией.

Логично, что коллизии возможны, так как размер int ограничен, да и реализации хэш-функции могут быть далеко не идеальны.

Реализация метода hashCode по-умолчанию возвращает разные значения hash -кодов для разных объектов:

Что это за значения? По-умолчанию hashCode() возвращает значение, которое называется идентификационный хэш (identity hash code). В документации сказано:

This is typically implemented by converting the internal address of the object into an integer, but this implementation technique is not required by the Java™ programming language.

Поэтому реализация зависит от JVM .

Более подробно об этом можно прочесть в разделе полезные ссылки.

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

Исходя из описания контракта метода можно понять, что hashCode , наряду с equals , играет важную роль в сравнении объектов. По сути он показывает изменилось ли состояние объекта, которое мы используем в equals для сравнения.

Именно поэтому, если вы переопределяете equals , то вы обязаны переопределить hashCode .

Самый очевидный и самый плохой пример как можно переопределить hashCode — это всегда возвращать константу:

Для равных объектов такой метод вернет одно и то же число, что удовлетворяет спецификации. Но, делать так категорически не рекомендуется.

Во-первых, если состояние объекта с такой реализацией hashCode , будет изменено, то это никак не отразится на hashCode . Что уже наталкивает на мысль, что такая реализация не совсем верна.

Во-вторых, такая реализация гарантирует возникновение коллизий всегда. А значит, использовать такой подход — это лишить себя всех преимуществ, которые дает нам использование ассоциативных массивов.

Теперь, когда мы решили, что возвращать всегда одно и то же значение — это плохое решение, давайте поговорим о том, что нужно включать в рассчет hashCode .

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

Далее необходимо выбрать базу: число, которое будет основной вычисления hash -кода.

По историческим причинам обычно за базу берут число 31 . Кто-то говорит, что это взято из-за близости к числу 32 , т.е степени двойки 2^5 — 1 . Кто-то утверждает, что был проведен эксперимент и наиболее лучшей базой получились числа 31 и 33 , а 31 понравилось больше.

В целом, вы можете выбрать за базу что хотите, но обычно выбирают 31 . Многие IDE (например, IDEA ) генерят hashCode именно с такой базой.

Правила вычисления hashCode :

Присваиваем переменной result ненулевое значение — базу.

Далее для каждого значимого поля в объекте вычисляем hashCode , по следующим правилам:

Если поле boolean — (f ? 1 : 0)

Если поле byte , char , short или int — (int) f

Если поле long — (int)(f ^ (f >>> 32))

Если поле float , то Float.floatToIntBits(f);

Если поле double , то Double.doubleToLongBits(f) , а затем как с long .

Если поле это ссылка на другой объект, то рекурсивно вызовите hashCode()

Если поле null , то возвращаем 0 .

Если поле это массив, то обрабатываем так, как будто каждый элемент массива — это поле объекта.

После каждого обработанного поля объединяем его hashCode с текущим значением:

Пример приведем с помощью многострадального класса Person :

Также, с версии Java 8+ в классе java.util.Objects есть вспомогательные методы для генерации hashCode :

Использование hashCode в equals

Так как hashCode допускает возникновение коллизий, то использовать его в equals нет смысла. Методы идут в паре, но использование одного в другом не желательно и бесмыссленно.

hashCode призван облегчить поиск объектов в каких-то структурах, например, ассоциативных массивах. equals — это уже непосредственно про сравнение объектов.

Классы-обертки над примитивами

Помните, что типы-обертки, которые по размеру меньше или равны int , возвращают в качестве hashCode свое значение.

Так как Long превосходит int , то, разумеется, возвращать свое значение в качестве hashCode он не может. Вместо этого используется более сложный алгоритм, который работает с значением long (как в примере Person -а).

Помните, что хэш-код массива не зависит от хранимых в нем элементов, а присваивается при создании массива!

Решением является использовать статический метод для рассчета hashCode у массивов: Arrays.hashCode(. ) :

Хэш — это некоторое число, генерируемое на основе объекта и описывающее его состояние. Метод hashCode , наряду с equals , играет важную роль в сравнении объектов. По сути он показывает изменилось ли состояние объекта, которое мы используем в equals для сравнения.

Если планируется использовать объекты в качестве ключа в ассоциативном маассиве, то переопределять hashCode обязательно. Если в классе переопределяется equals , то обязательно надо переопределять hashCode и наоборот.

Эти методы всегда должны определяться парой!

Плохая реализация hash -функции даст вам большое количество коллизий, что сведет на нет все преимущества использования ассоциативных массивов.

Помните, что большинство IDE сейчас легко сгенерируют вам hashCode , чтобы вы не писали его вручную.

Также, существуют сторонние проекты, которые берут кодогенерацию на себя, например, проект lombok. Существуют и сторонние библиотеки, помогающие в вычислении hashCode , например apache commons.

Методы equals() и hashcode() в языке Java

Методы equals и hashCode в Java в чём-то очень схожи, и даже вместе генерируются средствами IDE, таких как IntelliJ IDEA. Что в них общего, каковы отличия, и что будет, если использовать только один из методов?

Метод equals() в Java

Как вы наверняка знаете, сравнение посредством == в Java сравнивает ссылки, но объекты таким образом не сравнить. Следующий пример подобного сравнения двух строк вернёт false :

Пусть значения и одинаковы, но переменные String указывают на разные объекты.

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

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

Контракт equals() в Java

Используя equals , мы должны придерживаться основных правил, определённых в спецификации Java:

  1. Рефлексивность — x.equals(x) возвращает true .
  2. Симметричность — x.equals(y) <=> y.equals(x) .
  3. Транзитивность — x.equals(y) <=> y.equals(z) <=> x.equals(z) .
  4. Согласованность — повторный вызов x.equals(y) должен возвращать значение предыдущего вызова, если сравниваемые поля не изменялись.
  5. Сравнение null — x.equals(null) возвращает false .

Использование equals

Предположим, у нас есть класс Programmer , в котором предусмотрены поля с должность и зарплатой:

В переопределённом методе equals() обе переменные участвуют в проверке. Также вы всегда можете убрать ту переменную, которую не хотите проверять на равенство.

Зачастую метод equals в Java определяется вместе с hashCode, но здесь мы рассмотрим первый метод отдельно:

Определим объекты programmer1 и programmer2 типа Programmer с одинаковыми значениями. При их сравнении с помощью == вернётся false , так как это разные объекты. Если же мы используем для сравнения метод equals() , вернётся true :

А вот такой результат мы получим, если хотя бы одна переменная (обозначенное поле объекта) из метода equals() не совпадёт:

Метод hashcode() в Java

Наконец, мы дошли до сравнения методов equals и hashCode в языке Java.

Фундаментальное отличие в том, что hashCode() — это метод для получения уникального целочисленного номера объекта, своего рода его идентификатор. Благодаря хешу (номеру) можно, например, быстро определить местонахождение объекта в коллекции.

Это число используется в основном в хеш-таблицах, таких как HashMap . При этом хеш-функция получения числа на основе объекта должна быть реализована таким образом, чтобы обеспечить равномерное распределение элементов по хэш-таблице. А также минимизировать возможность появления коллизий, когда по разным ключам функция вернёт одинаковое значение.

В случае Java, метод hashCode() возвращает для любого объекта 32-битное число типа int . Сравнить два числа между собой гораздо быстрее, чем сравнить два объекта методом equals() , особенно если в нём используется много полей.

Контракт hashCode() в Java

  1. Повторный вызов hashCode для одного и того же объекта должен возвращать одинаковые хеш-значения, если поля объекта, участвующие в вычислении значения, не менялись.
  2. Если equals() для двух объектов возвращает true , hashCode() также должен возвращать для них одно и то же число.
  3. При этом неравные между собой объекты могут иметь одинаковый hashCode .

Использование hashCode

Вернёмся к нашему классу Programmer . По-хорошему, вместе с equals() должен быть использован и метод hashCode():

Почему equals и hashCode в Java переопределяются вместе

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

Рассмотрим два сценария.

1. equals есть, hashCode нет

С точки зрения метода equals два объекта будут логически равны, но по hashCode они не будут иметь ничего общего. Таким образом, помещая некий объект в хэш-таблицу, мы рискуем не получить его обратно по ключу:

2. hashCode есть, equals нет

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

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

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

В этом посте мы обсудим, почему важно переопределить equals() а также hashCode() метод в Java.

Пункт 9 в книге Джоша Блоха «Эффективная Java» предлагает всегда переопределять hashCode() метод, если класс переопределяет equals() . В этом посте мы обсудим, почему это необходимо и рекомендуемая практика.

Мы знаем, что два объекта считаются равными, только если их ссылки указывают на один и тот же объект, и если мы не переопределить методы equals и hashCode, объект класса не будет вести себя должным образом в коллекциях на основе хэшей, таких как HashMap , HashSet , а также Hashtable . Это связано с тем, что коллекции на основе хэшей организованы как последовательность сегментов, и значение хеш-кода объекта используется для определения сегмента, в котором будет храниться объект, и тот же хэш-код снова используется для нахождения позиции объекта в ведро. Поиск ключа представляет собой в основном двухэтапный процесс:

  1. Поиск правильного ведра с помощью hashCode() метод.
  2. Линейный поиск ключа в ведре с использованием equals() метод.

Теперь давайте возьмем пример, чтобы продемонстрировать необходимость переопределения equals а также hashCode метод в Java:

Как видно из сгенерированного вывода, набор содержит только один Employee объект, даже если это два разных Employee добавляются объекты. Это потому, что мы переопределили оба equals() а также hashCode() метод в Employee class, и оба объекта теперь указывают на одно и то же ведро и занимают одно и то же место в ведре.

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

1. Только переопределить equals() без переопределения hashCode() метод

Только переопределение equals() метод без переопределения hashCode() приводит к тому, что два одинаковых экземпляра имеют неравные хэш-коды, что нарушает контракт hashCode (упомянутый в Javadoc), который ясно говорит: если два объекта равны по equals(Object) метод, то вызов метода hashCode для каждого из двух объектов должен давать один и тот же целочисленный результат..

Поскольку реализация hashCode по умолчанию в Object класс возвращает разные целые числа для разных объектов, если только equals() метод переопределен, e1 будет помещен в некоторое ведро и e2 будет помещен в какое-то другое ведро как e1.hashCode() != e1.hashCode() . Хотя оба e1 а также e2 равны, они не относятся к одному и тому же сегменту, и оба находятся в коллекции как отдельные ключи.

Стоит отметить, что если экземпляр класса никогда не используется ни в каких коллекциях на основе хэшей, то не имеет большого значения, если hashCode() переопределяется или нет.

2. Только переопределение hashCode() без переопределения equals() метод

Если мы только переопределим hashCode() метод, оба e1 а также e2 будут хэшировать в одно и то же ведро, поскольку они производят один и тот же хеш-код. Но так как equals() метод не переопределяется, когда заданы хэши e2 и перебирает ведро, ища, есть ли Сотрудник e такой, что e2.equals(e) это правда, он не найдет ни одного e2.equals(e1) будет ложным.

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

Вот и все, почему нам нужно переопределить методы equals и hashcode в Java.

Читать:
Как установить word office 2010 бесплатно на компьютер

Похожие статьи