что будет если переопределить equals не переопределяя hashcode какие могут возникнуть проблемы
Разбираемся с hashCode() и equals()
Что такое хеш-код?
Если очень просто, то хеш-код — это число. На самом деле просто, не так ли? Если более точно, то это битовая строка фиксированной длины, полученная из массива произвольной длины (википедия).
Пример №1
Выполним следующий код:
Вторая часть объяснения гласит:
полученная из массива произвольной длины.
В итоге, в терминах Java, хеш-код — это целочисленный результат работы метода, которому в качестве входного параметра передан объект.
Подведём итог:
Сперва, что-бы избежать путаницы, определимся с терминологией. Одинаковые объекты — это объекты одного класса с одинаковым содержимым полей.
Понятие эквивалентности. Метод equals()
Начнем с того, что в java, каждый вызов оператора new порождает новый объект в памяти. Для иллюстрации создадим какой-нибудь класс, пускай он будет называться “BlackBox”.
Пример №2
Выполним следующий код:
Во втором примере, в памяти создастся два объекта.
Эквивалентность и хеш-код тесно связанны между собой, поскольку хеш-код вычисляется на основании содержимого объекта (значения полей) и если у двух объектов одного и того же класса содержимое одинаковое, то и хеш-коды должны быть одинаковые (см. правило 2).
Класс Object
При сравнение объектов, операция “ == ” вернет true лишь в одном случае — когда ссылки указывают на один и тот-же объект. В данном случае не учитывается содержимое полей.
Заглянем в исходный код метода hashCode() в классе Object :
При вычислении хэш-кода для объектов класса Object по умолчанию используется Park-Miller RNG алгоритм. В основу работы данного алгоритма положен генератор случайных чисел. Это означает, что при каждом запуске программы у объекта будет разный хэш-код.
Но, как мы помним, должно выполняться правило: “если у двух объектов одного и того же класса содержимое одинаковое, то и хеш-коды должны быть одинаковые ”. Поэтому, при создании пользовательского класса, принято переопределять методы hashCode() и equals() таким образом, что бы учитывались поля объекта.
Это можно сделать вручную либо воспользовавшись средствами генерации исходного кода в IDE. Например, в Eclipse это Source → Generate hashCode() and equals().
В итоге, класс BlackBox приобретает вид:
Теперь методы hashCode() и equals() работают корректно и учитывают содержимое полей объекта:
Кому интересно переопределение в ручную, можно почитать Effective Java — Joshua Bloch, chapter 3, item 8,9.
Java Challengers #4: Сравнение объектов с equals() и hashCode()
В преддверии запуска нового потока по курсу “Разработчик Java” мы продолжаем перевод серии статей Java Challengers, предыдущие части которых можно прочитать по ссылкам ниже:
В этой статье вы узнаете, как связаны между собой методы equals() и hashCode() и как они используются при сравнении объектов.
Без использования equals() и hashCode() для сравнения состояния двух объектов нам нужно писать много сравнений ” if “, сравнивая каждое поле объекта. Такой подход делает код запутанным и трудным для чтения. Работая вместе, эти два метода помогают создавать более гибкий и согласованный код.
Исходный код для статьи находится здесь.
Переопределение equals() и hashCode()
Переопределение метода (method overriding) — это приём при котором поведение родительского класса или интерфейса переписывается (переопределяется) в подклассе (см. Java Challengers #3: Полиморфизм и наследование, анг.). В Java у каждого объекта есть методы equals() и hashCode() и для правильной работы они должны быть переопределены.
Это native — метод, который написан на другом языке, таком как Си, и он возвращает некоторый числовой код, связанный с адресом памяти объекта. (Если вы не пишете код JDK, то не важно точно знать, как работает этот метод.)
Примечание переводчика: про значение, связанное с адресом сказано не совсем корректно (спасибо vladimir_dolzhenko). В HotSpot JVM по умолчанию используются псевдослучайные числа. Описание реализации hashCode() для HotSpot, есть здесь и здесь.
Сравнение объектов с equals()
Метод equals() используется для сравнения объектов. Чтобы определить одинаковые объекты или нет, equals() сравнивает значения полей объектов:
Во втором сравнении проверяется, является ли переданный объект null и какой у него тип. Если переданный объект другого типа, то объекты не равны.
Наконец, equals() сравнивает поля объектов. Если два объекта имеют одинаковые значения полей, то объекты совпадают.
Анализ вариантов сравнения объектов
Затем снова сравниваем два объекта Simpson :
Наконец, давайте сравним объект Simpson и экземпляр класса Object :
equals() в сравнении с ==
На первый взгляд кажется, что оператор == и метод equals() делают одно и то же, но, на самом деле, они работают по-разному. Оператор == сравнивает, указывают ли две ссылки на один и тот же объект. Например:
Во следующем примере используем переопределенный метод equals() :
Идентификация объектов с hashCode()
Использование equals() и hashCode() с коллекциями
Классы, реализующие интерфейс Set (множество) должны не допускать добавления повторяющихся элементов. Ниже приведены некоторые классы, реализующие интерфейс Set :
Посмотрим на часть реализации метода add() в HashSet :
Перед добавлением нового элемента HashSet проверяет, существует ли элемент в данной коллекции. Если объект совпадает, то новый элемент вставляться не будет.
Рекомендации по использованию equals() и hashCode()
Таблица 1. Сравнение хэш-кодов
Этот принцип в основном используется в коллекциях Set или Hash по соображениям производительности.
Правила сравнения объектов
Таблица 2.Сравнение объектов с hashCode()
Таблица 3. Сравнение объектов с equals()
Решите задачку на equals() и hashCode()
Для начала, внимательно изучите следующий код :
Сначала проанализируйте код, подумайте, какой будет результат. И только потом запустите код. Цель в том, чтобы улучшить ваши навыки анализа кода и усвоить основные концепции Java, чтобы вы могли сделать свой код лучше.
Какой будет результат?.
Что произошло? Понимание equals() и hashCode()
Первый объект в наборе будет вставлен как обычно:
Следующий объект также будет вставлен в обычном порядке, поскольку содержит значение, отличное от предыдущего объекта:
Наконец, следующий объект Simpson имеет то же значение имени, что и первый объект. В этом случае объект вставляться не будет:
Ответ
Правильный ответ — B. Вывод будет:
Частые ошибки с equals() и hashCode()
Что нужно помнить о equals() и hashCode()
Изучите больше о Java
Традиционно жду ваши комментарии и приглашаю на открытый урок, который уже 18 марта проведет наш преподаватель Сергей Петрелевич
Собеседование по Java – ООП (вопросы и ответы). Часть 3
Третья часть ответов и вопросов для собеседования по ООП в Java.
К списку вопросов по всем темам
Вопросы. Часть 3
43. Каким образом можно обратиться к локальной переменной метода из анонимного класса, объявленного в теле этого метода? Есть ли какие-нибудь ограничения для такой переменной?
44. Как связан любой пользовательский класс с классом Object?
45. Расскажите про каждый из методов класса Object.
46. Что такое метод equals(). Чем он отличается от операции ==.
47. Если вы хотите переопределить equals(), какие условия должны удовлетворяться для переопределенного метода?
48. Если equals() переопределен, есть ли какие-либо другие методы, которые следует переопределить?
49. В чем особенность работы методов hashCode и equals? Каким образом реализованы методы hashCode и equals в классе Object? Какие правила и соглашения существуют для реализации этих методов? Когда они применяются?
50. Какой метод возвращает строковое представление объекта?
51. Что будет, если переопределить equals не переопределяя hashCode? Какие могут возникнуть проблемы?
52. Есть ли какие-либо рекомендации о том, какие поля следует использовать при подсчете hashCode?
53. Как вы думаете, будут ли какие-то проблемы, если у объекта, который используется в качестве ключа в hashMap изменится поле, которое участвует в определении hashCode?
54. Чем отличается абстрактный класс от интерфейса, в каких случаях что вы будете использовать?
55. Можно ли получить доступ к private переменным класса и если да, то каким образом?
56. Что такое volatile и transient? Для чего и в каких случаях можно было бы использовать default?
57. Расширение модификаторов при наследовании, переопределении и сокрытии методов. Если у класса-родителя есть метод, объявленный как private, может ли наследник расширить его видимость? А если protected? А сузить видимость?
58. Имеет ли смысл объявлять метод private final?
59. Какие особенности инициализации final переменных?
60. Что будет, если единственный конструктор класса объявлен как final?
61. Что такое finalize? Зачем он нужен? Что Вы можете рассказать о сборщике мусора и алгоритмах его работы.
62. Почему метод clone объявлен как protected? Что необходимо для реализации клонирования?
Ответы. Часть 3
43. Каким образом можно обратиться к локальной переменной метода из анонимного класса, объявленного в теле этого метода? Есть ли какие-нибудь ограничения для такой переменной?
Также как и локальные классы, анонимные могут захватывать переменные, доступ к локальным переменным происходит по тем же правилам:
Анонимные классы также могут содержать в себе локальные классы. Конструктора в анонимном классе быть не может.
О сравнении объектов с помощью методов Java equals() и hashcode()
Без equals() и hashcode() нам пришлось бы создавать громоздкие сравнения «if», сопоставляя каждое поле с объектом. Что сделало бы исходный код запутанным.
Переопределение equals() и hashcode() в Java
Ниже приведен метод equals() в классе Object. Метод проверяет, совпадает ли текущий экземпляр с ранее переданным объектом.
Если equals() и hashcode() не переопределены, вместо них вы увидите приведенные выше методы. В этом случае методы не выполняют задачу equals() и hashcode() — проверку, имеют ли два (или более) объекта одинаковые значения.
Сравнение объектов с помощью метода equals ()
Мы используем метод equals() для сравнения объектов в Java. Чтобы определить, совпадают ли два объекта, equals() сравнивает значения атрибутов объектов:
Во втором сравнении equals() проверяет, является ли переданный объект пустым, или его тип принадлежит к другому классу. Если это другой класс, то объекты не равны.
Наконец, equals() сравнивает поля объектов. Если два объекта имеют одинаковые значения полей, то объекты одинаковы.
Анализ сравнений объектов
Рассмотрим результаты этих сравнений в методе main(). Сначала мы сравниваем два объекта Simpson:
Объекты идентичны, поэтому результат будет true.
Затем снова сравниваем два объекта Simpson:
Сравним объект Simpson и экземпляр класса Object :
В этом случае результат будет false, потому что типы классов разные.
equals () или ==
Может показаться, что оператор == и метод equals() делают то же самое. Но на самом деле они работают по-разному. Оператор == сравнивает, указывают ли две ссылки на один и тот же объект.
Идентификация объектов с помощью hashcode ()
Мы используем метод hashcode() для оптимизации производительности при сравнении объектов. Выполнение hashcode() возвращает уникальный идентификатор для каждого объекта в программе. Что значительно облегчает реализацию.
Использование equals() и hashcode() с коллекциями
Интерфейс Set отвечает за то, чтобы в подкласс Set не было повторяющихся элементов. Ниже перечислены часто используемые классы, реализующие интерфейс Set:
В приведенном ниже примере для добавления нового элемента в объект HashSet используется метод add. Перед добавлением нового элемента HashSet проверяет, существует ли элемент в данной коллекции:
Если объект тот же, новый элемент не будет вставлен.
Хэш-коллекции
Рекомендации по использованию equals() и hashcode()
Для объектов, имеющих один и тот же уникальный идентификатор hashcode, нужно использовать только equals(). Не нужно выполнять equals(), когда идентификатор hashcode отличается.
Таблица 1. Сравнение хэш-кодов
| Если сравнение hashcode() … | Тогда … |
| возвращает true | выполнить equals () |
| возвращает false | не выполнять equals () |
Этот принцип используется в коллекциях Set или Hash для повышения производительности.
Правила сравнения объектов
Таблица 2. Сравнение объектов с помощью hashcode()
| Когда сравнение хэш-кодов возвращает… | метод equals() должен возвращать … |
| True | true или false |
| False | False |
Таблица 3. Сравнение объектов с помощью equals()
| Когда метод equals() возвращает … | метод hashcode() должен возвращать… |
| True | True |
| False | true или false |
Выполните задание на использование equals() и hashcode()!
Для начала внимательно изучите приведенный ниже код:
Проанализируйте код, угадайте результат, а затем запустите программу. Ваша цель заключается в том, чтобы улучшить навыки анализа кода,усвоить основные концепции Java и сделать создаваемый код более эффективным. Выберите свой вариант, прежде чем проверять правильный ответ, который приведен ниже.
Что сейчас произошло? Понимание equals() и hashcode()
В первом сравнении equals() результат верен, потому что метод hashcode() возвращает одно и то же значение для обоих объектов.
Обратите внимание, что размер коллекции задан для хранения трех объектов Simpson. Рассмотрим это более подробно.
Первый объект в наборе будет добавлен в коллекцию:
Следующий объект также будет добавлен, поскольку имеет отличное от предыдущего объекта значение:
Последний объект Simpson имеет то же значение, что и первый. В этом случае объект не будет вставлен:
Объект overridenHomer использует другое значение хэш-кода из обычного экземпляра Simpson («Homer»). По этой причине этот элемент будет вставлен в коллекцию:
Ответ
Распространенные ошибки использования equals() и hashcode()
Что нужно помнить о equals() и hashcode()
Пожалуйста, опубликуйте ваши отзывы по текущей теме статьи. За комментарии, подписки, лайки, отклики, дизлайки огромное вам спасибо!
Переопределение Equals и GetHashCode. А оно надо?
Насколько серьезна эта проблема?
Не каждая потенциальная проблема с производительностью влияет на время выполнения приложения. Метод Enum.HasFlag не очень эффективен (*), но если не использовать его на ресурсоемком участке кода, то серьезных проблем в проекте не возникнет. Это верно и случае с защищенными копиями, созданными типами non-readonly struct в контексте readonly. Проблема существует, но вряд ли будет заметна в обычных приложениях.
Почему стандартные версии работают медленно?
Авторы CLR изо всех сил старались сделать стандартные версии Equals и GetHashCode максимально эффективными для типов значений. Но есть несколько причин, по которым эти методы проигрывают в эффективности пользовательской версии, написанной для определенного типа вручную (или сгенерированной компилятором).
(**) Если метод не поддерживает JIT-компиляцию. Например, в Core CLR 2.1 компилятор JIT распознает метод Enum.HasFlag и генерирует подходящий код, который не запускает упаковку-преобразование.
Традиционная хэш-функция типа struct «объединяет» хэш-коды всех полей. Но единственный способ получить хэш-код поля в методе ValueType — использовать рефлексию. Вот почему авторы CLR решили пожертвовать скоростью ради распределения, и стандартная версия GetHashCode только возвращает хэш-код первого ненулевого поля и «портит» его идентификатором типа (***) (подробнее см. RegularGetValueTypeHashCode в coreclr repo на github).
(***) Судя по комментариям в репозитории CoreCLR, в будущем ситуация может измениться.
Это разумный алгоритм пока что-то не пойдет не так. Но если вам не повезло и значение первого поля вашего типа struct совпадают в большинстве экземпляров, то хэш-функция всегда будет выдавать одинаковый результат. Как вы уже догадались, если сохранить эти экземпляры в хэш-наборе или хэш-таблице, то производительность резко упадет.
3. Скорость реализации на основе рефлексии низкая. Очень низкая. Рефлексия — это мощный инструмент, если использовать его правильно. Но последствия будут ужасны, если запустить его на ресурсоемком участке кода.
Давайте посмотрим, как неудачная хэш-функция, которая может получиться из-за (2) и реализации на основе рефлексии, влияет на производительность:
Как показывает тест, производительность будет приемлемой, если вам повезет и первый элемент структуры будет уникальным (в случае Position_Path_DefaultEquality ). Но если это не так, то производительность будет крайне низкой.
Реальная проблема
Думаю, теперь вы догадываетесь, с какой проблемой я недавно столкнулся. Пару недель назад я получил сообщение об ошибке: время выполнения приложения, над которым я работаю, увеличилось с 10 до 60 секунд. К счастью, отчет был весьма подробным и содержал трассировку событий Windows, поэтому проблемное место обнаружилось быстро — ValueType.Equals загружался 50 секунд.
После беглого просмотра кода стало ясно, в чем проблема:
Всегда ли реализация ValueType.Equals/GetHashCode по умолчанию работает медленно?
Но оптимизация – очень коварная вещь.
Во-первых, сложно понять, когда она включена; даже незначительные изменения в коде могут включать ее и выключать:
Во-вторых, сравнение памяти вовсе не обязательно даст вам правильный результат. Вот простой пример:
-0,0 и +0,0 равны, но имеют разные двоичные представления. Это значит, что Double.Equals окажется верным, а MyDouble.Equals — ложным. В большинстве случаев разница несущественна, но представьте, сколько часов вы потратите на исправление проблемы, вызванной этой разницей.
Как избежать подобной проблемы?
Вы можете спросить меня, как упомянутое выше может произойти в реальной ситуации? Один из очевидных способов запуска методов Equals и GetHashCode в типах struct — использование правила FxCop CA1815. Но есть одна проблема: это слишком строгий подход.
Приложение, для которого критически важна производительность, может иметь сотни типов struct, которые не обязательно используются в хэш-наборах или словарях. Поэтому разработчики приложения могут отключить правило, что вызовет неприятные последствия, если тип struct использует измененные функции.

