Создание переопределений для методов Equals и GetHashCode в Visual Studio
Что? Эта возможность позволяет создавать методы Equals и GetHashCode.
Когда? Создавайте эти переопределения, если у вас есть тип, который нужно сравнить по одному или нескольким полям, а не по расположению объектов в памяти.
Зачем?
- При реализации типа значения рекомендуется переопределить метод Equals. При этом обеспечивается повышенная производительность по сравнению с реализацией метода Equals по умолчанию для ValueType.
- При реализации ссылочного типа рекомендуется переопределить метод Equals, если ваш тип выглядит как базовый, например Point, String, BigNumber и т. д.
- Переопределите метод GetHashCode, чтобы тип правильно работал в хэш-таблице. Дополнительные сведения см. в руководстве по операторам равенства.
Практические советы
- Поместите курсор в любую позицию на строке объявления типа.
public class ImaginaryNumber < public double RealNumber < get; set; >public double ImaginaryUnit < get; set; >>
Теперь код должен выглядеть как на следующем снимке экрана:
Совет Не выбирайте имя типа двойным щелчком, иначе параметр меню будет недоступен. Просто установите курсор в любую позицию на строке.
- Нажмите CTRL+., чтобы открыть меню Быстрые действия и рефакторинг.
- Щелкните правой кнопкой мыши и выберите меню Быстрые действия и рефакторинг.
- Щелкните значок, который отображается в левом поле.
Совет В этом диалоговом окне также можно создавать операторы, используя флажок в нижней части окна.
Методы Equals и GetHashCode создаются с реализациями по умолчанию, как показано в следующем коде:
public class ImaginaryNumber : IEquatable < public double RealNumber < get; set; >public double ImaginaryUnit < get; set; >public override bool Equals(object obj) < return Equals(obj as ImaginaryNumber); >public bool Equals(ImaginaryNumber other) < return other != null && RealNumber == other.RealNumber && ImaginaryUnit == other.ImaginaryUnit; >public override int GetHashCode() < return HashCode.Combine(RealNumber, ImaginaryUnit); >>
Теперь код должен выглядеть как на следующем снимке экрана:
См. также
- Создание кода
- Просмотр изменений
Кофе-брейк #168. Зачем переопределять методы equals и hashcode в Java?
Зачем переопределять методы equals и hashcode в Java?
Источник: 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()?
Некоторые структуры данных в 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");
В классе 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");
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,269 2 2 золотых знака 3 3 серебряных знака 19 19 бронзовых знаков
== не будет работать, если вы просто переопределите эти методы. Для их работы нужно переопределять сами операторы дополнительно.
Контракты equals и hashCode или как оно всё там
Преобладающее большинство программирующих на Java конечно же знают, что методы equals и hashCode тесно связаны друг с другом, и что оба этих метода желательно переопределять в своих классах согласованно. Чуть меньшее количество знают, почему это так и какие печальные последствия могут быть, если нарушить данное правило. Предлагаю рассмотреть концепцию этих методов, повторить их назначение и разобраться, почему они так связаны. Эту статью, как и предыдущую про загрузку классов, я писал для себя, чтобы окончательно раскрыть все детали вопроса и больше не возвращаться к сторонним источникам. Поэтому буду рад конструктивной критике, т. к. если где-то есть пробелы, их следует устранить. Статья, увы, получилась достаточно объемная.
Правила переопределения equals
Метод equals() необходим в Java для подтверждения или отрицания того факта, что два объекта одного происхождения являются логически равными. То есть, сравнивая два объекта, программисту необходимо понять, эквивалентны ли их значимые поля. Не обязательно все поля должны быть идентичны, так как метод equals() подразумевает именно логическое равенство. Но иногда нет особой необходимости в использовании этого метода. Как говорится, самый легкий путь избежать проблем, используя тот или иной механизм — не использовать его. Также следует заметить, что однажды нарушив контракт equals вы теряете контроль над пониманием того, как другие объекты и структуры будут взаимодействовать с вашим объектом. И впоследствии найти причину ошибки будет весьма затруднительно.
Когда не стоит переопределять этот метод
- Когда каждый экземпляр класса является уникальным. В большей степени это касается тех классов, которые предоставляют определенное поведение, нежели предназначены для работы с данными. Таких, например, как класс Thread . Для них реализации метода equals , предоставляемого классом Object , более чем достаточно. Другой пример — классы перечислений ( Enum ).
- Когда на самом деле от класса не требуется определять эквивалентность его экземпляров. Например для класса java.util.Random вообще нет необходимости сравнивать между собой экземпляры класса, определяя, могут ли они вернуть одинаковую последовательность случайных чисел. Просто потому, что природа этого класса даже не подразумевает такое поведение.
- Когда класс, который вы расширяете, уже имеет свою реализацию метода equals и поведение этой реализации вас устраивает. Например, для классов Set , List , Map реализация equals находится в AbstractSet , AbstractList и AbstractMap соответственно.
- И, наконец, нет необходимости перекрывать equals , когда область видимости вашего класса является private или package-private и вы уверены, что этот метод никогда не будет вызван.
Контракт equals
- Рефлексивность для любого заданного значения x , выражение x.equals(x) должно возвращать true .
Заданного — имеется в виду такого, что x != null - Симметричность для любых заданных значений x и y , x.equals(y) должно возвращать true только в том случае, когда y.equals(x) возвращает true .
- Транзитивность для любых заданных значений x , y и z , если x.equals(y) возвращает true и y.equals(z) возвращает true , x.equals(z) должно вернуть значение true .
- Согласованность для любых заданных значений x и y повторный вызов x.equals(y) будет возвращать значение предыдущего вызова этого метода при условии, что поля, используемые для сравнения этих двух объектов, не изменялись между вызовами.
- Сравнение null для любого заданного значения x вызов x.equals(null) должен возвращать false .
Нарушение контракта equals
Многие классы, например классы из Java Collections Framework, зависят от реализации метода equals() , поэтому не стоит им пренебрегать, т.к. нарушение контракта этого метода может привести к нерациональной работе приложения и в таком случае найти причину будет достаточно трудно. Согласно принципу рефлексивности, каждый объект должен быть эквивалентен самому себе. Если этот принцип будет нарушен, при добавлении объекта в коллекцию и при последующем поиске его с помощью метода contains() мы не сможем найти тот объект, который только что положили в коллекцию. Условие симметричности гласит, что два любых объекта должны быть равны независимо от того, в каком порядке они будут сравниваться. Например, имея класс, содержащий всего одно поле строкового типа, будет неправильно сравнивать в методе equals данное поле со строкой. Т.к. в случае обратного сравнения метод всегда вернет значение false .
// Нарушение симметричности public class SomeStringify < private String s; @Override public boolean equals(Object o) < if (this == o) return true; if (o instanceof SomeStringify) < return s.equals(((SomeStringify) o).s); >// нарушение симметричности, классы разного происхождения if (o instanceof String) < return s.equals(o); >return false; > >
//Правильное определение метода equals @Override public boolean equals(Object o)
Из условия транзитивности следует, что если любые два из трех объектов равны, то в таком случае должны быть равны все три. Этот принцип легко нарушить в том случае, когда необходимо расширить некий базовый класс, добавив к нему значимый компонент. Например, к классу Point с координатами x и y необходимо добавить цвет точки, расширив его. Для этого потребуется объявить класс ColorPoint с соответствующим полем color . Таким образом, если в расширенном классе вызывать метод equals родителя, а в родительском будем считать, что сравниваются только координаты x и y , тогда две точки разного цвета, но с одинаковыми координатами будут считаться равными, что неправильно. В таком случае, необходимо научить производный класс различать цвета. Для этого можно воспользоваться двумя способами. Но один будет нарушать правило симметричности, а второй — транзитивности.
// Первый способ, нарушая симметричность // Метод переопределен в классе ColorPoint @Override public boolean equals(Object o)
В этом случае вызов point.equals(colorPoint) вернет значение true , а сравнение colorPoint.equals(point) — false , т.к. ожидает объект “своего” класса. Таким образом и нарушается правило симметричности. Второй способ подразумевает делать “слепую” проверку, в случае, когда нет данных о цвете точки, т. е. имеем класс Point . Или же проверять цвет, если информация о нем доступна, т. е. сравнивать объект класса ColorPoint .
// Метод переопределен в классе ColorPoint @Override public boolean equals(Object o) < if (!(o instanceof Point)) return false; // Слепая проверка if (!(o instanceof ColorPoint)) return super.equals(o); // Полная проверка, включая цвет точки return super.equals(o) && ((ColorPoint) o).color == color; >
Принцип транзитивности здесь нарушается следующим образом. Допустим, есть определение следующих объектов:
ColorPoint p1 = new ColorPoint(1, 2, Color.RED); Point p2 = new Point(1, 2); ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
Таким образом хоть и выполняется равенство p1.equals(p2) и p2.equals(p3) , p1.equals(p3) вернет значение false . При этом второй способ, на мой взгляд, выглядит менее привлекательным, т.к. в некоторых случаях алгоритм может ослепнуть и не выполнить сравнение в полной мере, а вы об этом можете и не узнать. Немного лирики В общем-то конкретного решения этой проблемы, как я понял, нет. Есть мнение одного авторитетного автора по имени Кей Хорстманн, что можно заменить использование оператора instanceof на вызов метода getClass() , который возвращает класс объекта и, прежде чем начать сравнивать сами объекты, убедиться, что они одного типа, а на факт их общего происхождения не обращать внимания. Таким образом, правила симметричности и транзитивности будут выполнены. Но при этом на другой стороне баррикады стоит еще один не менее уважаемый в широких кругах автор Джошуа Блох, который считает, что такой подход нарушает принцип подстановки Барбары Лисков. Этот принцип гласит, что “вызывающий код должен работать с базовым классом точно так же, как и с его подклассами, не зная об этом”. И в решении, предлагаемом Хорстманном, этот принцип явно нарушается, т. к. зависит от реализации. Короче дело ясное, что дело темное. Следует также отметить, что Хорстманн уточняет правило применения своего подхода и английским по белому пишет, что нужно определиться со стратегией при проектировании классов, и если проверка на равенство будет проводиться только силами суперкласса, можно это делать, выполняя операцию instanceof . Иначе, когда семантика проверки меняется в зависимости от производного класса и реализацию метода требуется спустить вниз по иерархии, необходимо использовать метод getClass() . Джошуа Блох, в свою очередь, предлагает отказаться от наследования и воспользоваться композицией объектов, включив в состав класса ColorPoint класс Point и предоставив метод доступа asPoint() для получения информации конкретно о точке. Это позволит избежать нарушения всех правил, но, как по мне, затруднит понимание кода. Третий вариант — воспользоваться автоматической генерацией метода equals с помощью IDE. Idea, кстати, воспроизводит генерацию по Хорстманну, причем позволяя выбрать стратегию реализации метода в суперклассе или в его наследниках. И, наконец, следующее правило согласованности гласит, что если объекты x и y не меняются, повторный вызов x.equals(y) должен вернуть то же значение, что и ранее. Последнее правило заключается в том, что ни один объект не должен быть равен null . Здесь все понятно, null — это неопределенность, равен ли объект неопределенности? Непонятно, т. е. false .
Общий алгоритм определения equals
- Проверить на равенство ссылки объектов this и параметра метода o .
if (this == o) return true; - Проверить, определена ли ссылка o , т. е. является ли она null .
Если в дальнейшем при сравнении типов объектов будет использоваться оператор instanceof , этот пункт можно пропустить, т. к. этот параметр возвращает false в данном случае null instanceof Object . - Сравнить типы объектов this и o с помощью оператора instanceof или метода getClass() , руководствуясь описанием выше и собственным чутьем.
- Если метод equals переопределяется в подклассе, не забудьте сделать вызов super.equals(o)
- Выполнить преобразование типа параметра o к требуемому классу.
- Выполнить сравнение всех значимых полей объектов:
- для примитивных типов (кроме float и double ), используя оператор ==
- для ссылочных полей необходимо вызвать их метод equals
- для массивов можно воспользоваться перебором по циклу, либо методом Arrays.equals()
- для типов float и double необходимо использовать методы сравнения соответствующих оберточных классов Float.compare() и Double.compare()
- И, наконец, ответить на три вопроса: является ли реализованный метод симметричным? Транзитивным? Согласованным? Два других принципа (рефлексивность и определенность), как правило, выполняются автоматически.
Правила переопределения hashCode
Хэш — это некоторое число, генерируемое на основе объекта и описывающее его состояние в какой-то момент времени. Это число используется в Java преимущественно в хэш-таблицах, таких как HashMap . При этом хэш-функция получения числа на основе объекта должна быть реализована таким образом, чтобы обеспечить относительно равномерное распределение элементов по хэш-таблице. А также минимизировать вероятность появления коллизий, когда по разным ключам функция вернет одинаковое значение.
Контракт hashCode
- вызов метода hashCode один и более раз над одним и тем же объектом должен возвращать одно и то же хэш-значение, при условии что поля объекта, участвующие в вычислении значения, не изменялись.
- вызов метода hashCode над двумя объектами должен всегда возвращать одно и то же число, если эти объекты равны (вызов метода equals для этих объектов возвращает true ).
- вызов метода hashCode над двумя неравными между собой объектами должен возвращать разные хэш-значения. Хотя это требование и не является обязательным, следует учитывать, что его выполнение положительно повлияет на производительность работы хэш-таблиц.
Методы equals и hashCode необходимо переопределять вместе
-
equals есть, hashCode нет Допустим мы правильно определили метод equals в нашем классе, а метод hashCode решили оставить как он есть в классе Object . Тогда с точки зрения метода equals два объекта будут логически равны, в то время как с точки зрения метода hashCode они не будут иметь ничего общего. И, таким образом, помещая некий объект в хэш-таблицу, мы рискуем не получить его обратно по ключу.
Например, так:
Map m = new HashMap<>(); m.put(new Point(1, 1), “Point A”); // pointName == null String pointName = m.get(new Point(1, 1));
Общий алгоритм определения hashCode
Здесь, мне кажется, вообще не стоит сильно переживать и выполнить генерацию метода в своей любимой IDE. Потому что все эти смещения битов вправо, влево в поиске золотого сечения, т. е. нормального распределения — это для совсем упоротых чуваков. Лично я сомневаюсь, что смогу сделать лучше и быстрее, чем та же Idea.
Вместо заключения
Таким образом, мы видим, что методы equals и hashCode играют четко определенную роль в языке Java и предназначены для получения характеристики логического равенства двух объектов. В случае с методом equals это имеет прямое отношение к сравнению объектов, в случае с hashCode косвенное, когда необходимо, скажем так, определить примерное расположение объекта в хэш-таблицах или подобных структурах данных с целью увеличения скорости поиска объекта. Помимо контрактов equals и hashCode имеется еще одно требование, относящееся к сравнению объектов. Это согласованность метода compareTo интерфейса Comparable с методом equals . Данное требование обязывает разработчика всегда возвращать x.equals(y) == true , когда x.compareTo(y) == 0 . Т. е. мы видим, что логическое сравнение двух объектов не должно противоречить нигде в приложении и всегда быть согласованным.
Источники
Effective Java, Second Edition. Joshua Bloch. Свободный перевод очень неплохой книги. Java, библиотека профессионала. Том 1. Основы. Кей Хорстманн. Чуть менее теории и более практики. Но не так подробно разобрано все, как у Блоха. Хотя есть свой взгляд на тот же equals(). Структуры данных в картинках. HashMap Крайне полезная статья по устройству HashMap в Java. Вместо того, чтобы исходники смотреть.