В этой статье мы рассмотрим два важных метода класса Object в Java - equals() и hashCode(), разберемся, зачем они нужны и расскажем об их использовании на конкретных примерах.
Зачем нужны equals() и hashCode() и в чём их особенность?
При программировании нередко возникают ситуации, когда нужно сравнивать объекты. Первое, что приходит на ум, когда хочется что-то сравнить, это проверить условие написав символ “==”. Кажется, что это должно решить все наши проблемы, однако это не так. Символ “==” имеет смысл использовать только с примитивами. Что касается объектов, “==” в Java сравнивает только ссылки, то есть проверяет, равен ли объект самому себе, а это не всегда может быть полезно. Для сравнения объектов в классе Object присутствует метод equals(), именно он и должен выполнять сравнение объектов. По умолчанию этот метод выполняет внутри себя операцию “==”, однако, идея этого метода состоит в том, что он должен быть переопределён для создаваемых классов.
Каким же должен быть переопределённый метод equals()?
Метод выглядит следующим образом: public boolean equals(Object o);
На вход он принимает Object и должен вернуть true или false. Теперь пройдемся по шагам.
Как же нужно произвести сравнение двух объектов в Java, чтобы оно выполнялось качественно и эффективно?
1) При сравнении двух объектов, стоит начать с проверки ссылок. Ведь если мы сравниваем объект с самим собой, зачем нам выполнять лишние проверки и сравнивать поля объекта.
2) Прежде чем приступать к глубокому сравнению, нужно выполнить еще 2 проверки, которые так же могут отсеять объекты, которые точно не равны. Это поможет ускорить работу метода equals(). Это проверки помогают понять, не является ли объект null и сравниваем ли мы объекты одинаковых классов. Если мы изначально сравниваем два null объекта, то первая проверка вернёт нам true, а значит ко второму шагу объект точно не должен быть null.
3) Теперь, когда мы уверены что оба объекта относятся к одинаковому классу и не равны null, можно выполнить приведение объекта к нужному типу и приступить к сравнению полей.
Пример
А вот и пример переопределённого метода, который у нас получился:
Выглядит неплохо, но стоит отметить, что вызываемые методы equals() для String и Integer уже переопределены внутри этих классов. Если полем вашего класса является объект другого вашего класса, в нём так же необходимо переопределять метод equals(). Кроме того, представленный метод полностью соответствует определённым для него требованиям, прописанным в документации Oracle, а именно:
1) Рефлексивность. Любой объект не равный null должен быть equals() самому себе.
2) Симметричность. Если a.equals(b) == true, то и b.equals(a) должно возвращать true.
3) Транзитивность. Если два объекта равны какому-то третьему объекту, значит, они должны быть равны друг и другу.
4) Постоянность. Результаты работы equals() должны меняться только при изменении входящих в него полей. Если данные двух объектов не менялись, результаты проверки на equals() должны быть всегда одинаковыми.
5) Неравенство с null. Для любого объекта не равного null проверка a.equals(null) должна возвращать false
Но что делать если нам нужно лишь убедиться в том, что объекты не равны друг другу и сделать это очень быстро?
Такой подход используется, когда нам нужно хранить набор уникальных объектов, как например в такой структуре данных как HashMap. В этом случае метод equals() не очень подходит, так как проверка всех полей объекта занимает время. В этом случае используется хэш-код объекта.
В Java хэш-код объекта можно получить с помощью метода hashCode(). Для любого объекта этот метод должен возвращать 32-битное значение типа int. Возвращаемый хэш-код должен удовлетворять требованиям, прописанным в документации Oracle, а именно:
1) Если два объекта равны (т. е. метод equals() возвращает true), у них должен быть одинаковый хэш-код.
2) Если метод hashCode() вызывается несколько раз на одном и том же объекте, каждый раз он должен возвращать одно и то же число.
3) Одинаковый хэш-код может быть у двух разных объектов.
Последнее правила вытекает из того, что хэш-код ограничен 32 битами, т. е. может принимать чуть больше 4 миллиардов различных значений, при этом количество создаваемых объектов не ограничено ничем, кроме памяти доступной приложению.
Пример
Рассмотрим пример того, как может быть переопределён метод hashCode() на примере нашего класса:
Умножение на простое нечётное число 31 используется для уменьшения числа коллизий, т. е. чтобы разнообразие рассчитываемых хэш-кодов было как можно больше. Есть и более приятный глазу способ переопределения хэш-кода. В классе Objects есть метод hash(), на вход которого нужно подать все поля класса. Этот метод делает то же самое что показано на предыдущей картинке:
Использование хэш-кода позволяет эффективно работать с элементами в HashMap. О том как именно достигается эта эффективность HashMap, какие проблемы она решает и какие проблемы могут возникнуть при работе с ней, мы расскажем в одной из следующих статей.