Больше не нужно искать работу мечты — присоединяйтесь к команде Клауда

Переопределение метода hashCode в Java

Илья Ушаков
Илья Ушаков
Технический писатель
23 декабря 2022 г.
3184
7 минут чтения
Средний рейтинг статьи: 5

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

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

Как видно, рисунок в точности описывает определение сюръекции. Для всех элементов из множества B существует хотя бы один сопоставленный элемент из множества A. Единственное отличие метода hashCode() от сюръекции – любой объект может быть обработан данным методом.

В процессе использования рассматриваемого метода могут возникнуть ситуации, когда у разных объектов будет одинаковый hash. Например, как видно по рисунку выше объекты C и V сопоставлены с одним и тем же элементом. Такие случаи называются коллизиями. Исправить их поможет метод equals(). Он тесно связан с методом hashCode(). Более подробно мы писали о нем в отдельной статье.

Image1

Если в проекте пользователь планирует использовать ассоциативный массив, а в качестве ключей в нем будут объекты, то метод hashCode() рекомендуется переопределять — для более быстрой и корректной работы. Также его необходимо переопределять в тех случаях, когда было выполнено переопределение метода equals(). О том, как правильно выполнять переопределение метода hashСode() в Java, будет рассказано немного позже.

Требования к реализации метода hashCode()

Объявление метода hashCode() выглядит следующим образом:

public int hashCode() {
   // ...
}

Для правильной реализации рассматриваемого метода в Java определен список следующих требований:

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

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

Переопределение метода hashCode()

Первое, что придет в голову пользователю, который захочет выполнить переопределение hashCode() в Java, – это сделать возвращение константы:

@Override
public int hashCode() {
    return 7;
}

Реализовывать переопределение подобным образом категорически нельзя, и на это есть свои причины:

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

Исходя из вышеперечисленных недостатков, от подобного переопределения метода необходимо отказаться.

Существует два варианта правильного переопределения метода hashCode() в Java:

  1. Использование существующего алгоритма для собственной реализации переопределения. 
  2. Использование вспомогательных методов для генерации хэш-кода.

Первый вариант предполагает выполнение следующих правил для переопределения hashCode() в Java:

  1. Во-первых, необходимо исключить все избыточные поля, которые не участвуют в equals().
  2. Во-вторых, следует выбрать базу — стартовое число, необходимое для расчета hash-кода объекта и присвоить его переменной total. Зачастую разработчики берут число 31, но вы можете выбрать иное значение. Многие IDE выполняют генерацию хэш-кода именно с этим числом.
  3. Далее для каждого из оставшихся полей после исключения проводится расчет хэша. Ниже будет приведена таблица правил вычисления для возможных типов полей:

Тип поля

Правило

boolean

(f ? 1 : 0)

char, short, byte или int

(int) f

float

Float.floatToIntBits(f)

double

Double.doubleToLongBits(f), а затем (int)(f ^ (f >>> 32))

long

(int)(f ^ (f >>> 32))

Ссылка на другой объект

Рекурсивный вывод метода hashCode()

Массив

Обработать каждый элемент массива, как отдельное поле объекта

null

return 0

  1. Следующее правило гласит, что нужно прибавить рассчитанный хэш каждого из полей (допустим, это переменная compute) к переменной total:
total = 31 * total + compute;
  1. И наконец, необходимо вернуть итоговое значение переменной total после выполнения всех расчетов.

Приведем пример реализации переопределения метода для класса Staff, используя перечисленные выше правила:

public class Staff {
    private String FCs;
    private String city;
    private int experience;
    private double wage;
    private String department;

    public Staff(String FCs, String city, int experience, double wage, String department) {
        this.FCs = FCs;
        this.city = city;
        this.experience = experience;
        this.wage = wage;
        this.department = department;
    }

    @Override
public int hashCode() {
        int total = 31; 
 
        total = total * 31 + (FCs == null ? 0 : FCs.hashCode()); 
        total = total * 31 + (city == null ? 0 : city.hashCode()); 
        total = total * 31 + experience; 
        long lwage = Double.doubleToLongBits(wage); 
        total = total * 31 + (int)(lwage ^ (lwage >>> 32)); 
        total = total * 31 + (department == null ? 0 : department.hashCode()); 
 
        return total; 
    }

    // Переопределение equals()
    // ...
}

Аннотация @Override перед объявлением рассматриваемого метода проверяет, что переопределяемый метод есть в родительском классе

Все строковые поля (FCs, city, и department) проверяются на null перед вызовом их метода hashCode(), чтобы обеспечить безопасность от NullPointerException.

Второй вариант предполагает использование вспомогательных методов для генерации хэш-кода, которые доступны благодаря классу java.util.Objects, начиная с версии Java 8+. Пример данной реализации приведен ниже: 

@Override
public int hashCode() {
    return Objects.hash(FCs, city, experience, department);
}

У всех стандартных ссылочных типов данных в Java (String, Integer, Double и т. д.) методы equals() и hashCode() уже корректно переопределены. Поэтому их возможно спокойно интегрировать в коллекции HashMap, HashSet и другие.

Что запомнить

  • Hash — это некое число, которое генерируется для объекта, благодаря hash-функциям, в том числе hashCode().
  • Метод hashCode() возвращает целочисленное значение hash-кода для выбранного объекта.
  • Если в проекте пользователь планирует использовать ассоциативный массив, а в качестве ключей в нем будут объекты, то метод hashCode() рекомендуется переопределять.
  • Правильно подобранная реализация метода hashCode() ускорит работу ассоциативных массивов.
  • При переопределении equals() нужно не забывать про переопределение hashCode(), и наоборот.
  • Неправильная реализация рассматриваемого в статье метода обеспечит пользователя большим количеством коллизий.
23 декабря 2022 г.
3184
7 минут чтения
Средний рейтинг статьи: 5
Хотите внести свой вклад?
Участвуйте в нашей контент-программе за
вознаграждение или запросите нужную вам инструкцию
img-server
Комментарии 2
Сашач
Сашач
15.03.2024, 12:26
Не совсем понятно почему при переопределении hashCode() для поля department (String) использован тернарный оператор, а для других полей с типом String (FCs, city) нет. Возможно, забыли дописать.
Timeweb
Timeweb
18.03.2024, 08:01
Спасибо, что обратили внимание! :blue_heart: Мы поправили блок кода в инструкции.