JavaRush /Курсы /Модуль 2. Java Core /Методы equals и hashCode

Методы equals и hashCode

Модуль 2. Java Core
9 уровень , 1 лекция
Открыта

— Теперь я расскажу о не менее полезных методах equals(Object o) & hashCode().

Как ты уже, наверное, успел запомнить, в Java при сравнении ссылочных переменных сравниваются не сами объекты, а ссылки на объекты.

Код Пояснение
Integer i = new Integer(1);
Integer j = new Integer(1);
System.out.println(i==j);
i не равно j
Переменные указывают на различные объекты.
Хотя объекты содержат одинаковые данные;
Integer i = new Integer(1);
Integer j = i;
System.out.println(i==j);
i равно j Переменные содержат ссылку на один и тот же объект.

— Да, я это помню.

— Есть также стандартное решение этой ситуации – метод equals.

Цель метода equals – определить идентичны ли объекты внутри, сравнив внутреннее содержание объектов.

— И как он это делает?

— Тут все аналогично методу toString().

У класса Object есть своя реализация метода equals, которая просто сравнивает ссылки:

public boolean equals(Object obj) {
    return (this == obj);
}

— М-да. С чем боролись, на то и напоролись.

— Не вешай нос. Тут все тоже очень хитро.

Этот метод создавался, чтобы разработчики переопределяли его в своих классах. Ведь только разработчик класса знает, какие данные важны, что учитывать при сравнении, а что – нет.

— А можно пример такого метода?

— Конечно. Допустим, у нас есть класс, описывающий математические дроби, тогда он выглядел бы так (для ясности, я переведу английские названия на русский язык):

Пример
class Дробь {
    private int числитель;
    private int знаменатель;

    Дробь(int числитель, int знаменатель) {
        this.числитель = числитель;
        this.знаменатель = знаменатель;
    }

    public boolean equals(Object obj) {
        if (obj == null)
            return false;

        if (obj.getClass() != this.getClass())
            return false;

        Дробь other = (Дробь) obj;
        return this.числитель * other.знаменатель == this.знаменатель * other.числитель;
    }
}
Пример вызова:
Дробь one = new Дробь(2,3);
Дробь two = new Дробь(4,6);
System.out.println(one.equals(two));
Результат вызова будет true.
дробь 2/3 равна дроби 4/6

— Для большей ясности я использовала русские названия. Так можно делать только в обучающих целях.

Теперь разберем пример.

Мы переопределили метод equals, и теперь для объектов класса Дробь у него будет своя реализация.

В этом методе есть несколько проверок:

1) Если переданный для сравнения объект – null, то объекты не равны. Объект, у которого вызвали метод equals ведь точно не null.

2) Проверка на сравнение классов. Если объекты разных классов, то мы не будем пробовать их сравнить, а сразу скажем, что это различные объекты – return false.

3) Со второго класса школы все помнят, что дробь 2/3 равна дроби 4/6. А как это проверить?

2/3 == 4/6
Умножим обе части на оба делителя (6 и 3), получим:
6 * 2 == 4 * 3
12 == 12
Общее правило:
Если
a / b == c / d
То
a * d == c * b

— Поэтому в третьей части метода equals мы преобразуем переданный объект к типу Дробь и сравниваем дроби.

— Понятно. Если бы мы просто сравнивали числитель с числителем и знаменатель со знаменателем, то дробь 2/3 не была бы равной 4/6.

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

— Да, но это только половина дела. Есть еще второй метод – hashCode()

— С методом equals все понятно, а зачем нужен hashCode()?

— Метод hashCode нужен для быстрого сравнения.

У метода equals есть большой минус – он слишком медленно работает. Допустим, у тебя есть множество(Set) из миллиона элементов, и нам нужно проверить, содержит ли оно определенный объект или нет. Как это сделать?

— Можно в цикле пройтись по всем элементам и сравнить нужный объект с каждым объектом множества. Пока не найдем нужный.

— А если его там нет? Мы выполним миллион сравнений, чтобы узнать, что там нет этого объекта? Не многовато ли?

— Да, даже мне понятно, что слишком много сравнений. А что, есть другой способ?

— Да, для этого и используется hashCode().

Метод hashCode() для каждого объекта возвращает определенное число. Какое именно – это тоже решает разработчик класса, как и в случае с методом equals.

Давай рассмотрим ситуацию на примере:

Представь, что у тебя есть миллион 10-тизначных чисел. Тогда в качестве hashCode для каждого числа можно выбрать остаток от его деления на 100.

Пример:

Число Наш hashCode
1234567890 90
9876554321 21
9876554221 21
9886554121 21

— Да, с этим понятно. И что нам делать с этим hashCode-числом?

— Вместо того чтобы сравнивать числа, мы будем сравнивать их hashCode. Так быстрее.

И только если hashCode-ы равны, сравнивать уже посредством equals.

— Да, так быстрее. Но нам все равно придется сделать миллион сравнений, только уже более коротких чисел, а для тех чисел, чьи hashCode совпадают, опять вызвать equals.

— Нет, можно обойтись гораздо меньшим числом.

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

Представь, что ты студент, и ищешь своего друга, которого знаешь в лицо и про которого известно, что он живет в 17 общаге. Тогда ты просто проходишься по всем общежитиям универа и в каждом общежитии спрашиваешь «это 17 общага?». Если нет, то ты отбрасываешь всех из этой общаги и переходишь к следующей. Если «да», то начинаешь ходить по всем комнатам и искать друга.

В данном примере номер общаги – 17 – это и есть hashCode.

Разработчик, который реализует функцию hashCode, должен знать следующие вещи:

А) у двух разных объектов может быть одинаковый hashCode (разные люди могут жить в одной общаге)

Б) у одинаковых объектов (с точки зрения equalsдолжен быть одинаковый hashCode.

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

И теперь самое важное. Если ты переопределяешь метод equals, обязательно нужно переопределить метод hashCode(), с учетом трех вышеописанных правил.

Все дело в том, что коллекции в Java перед тем как сравнить объекты с помощью equals всегда ищут/сравнивают их с помощью метода hashCode(). И если у одинаковых объектов будут разные hashCode, то объекты будут считаться разными - до сравнения с помощью equals просто не дойдет.

В нашем примере с Дробью, если бы мы взяли hashCode равный числителю, то дроби 2/3 и 4/6 имели бы разные hashCode. Дроби – одинаковые, equals говорит, что они одинаковые, но hashCode говорит, что они разные. И если перед сравнением с помощью equals сравнивать по hashCode, то получим что объекты разные и до equals просто не дойдём.

Пример:

HashSet<Дробь>set = new HashSet<Дробь>();
set.add(new Дробь(2,3));System.out.println( set.contains(new Дробь(4,6)) );
Если метод hashCode() будет возвращать числитель дроби, то результат будет false.
Объект new Дробь(4,6) не будет найден в коллекции.

— А как правильно реализовать hashCode для дроби?

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

Вариант 1: hashCode равен целой части от деления.

Для дроби 7/5 и 6/5 это будет 1.

Для дроби 4/5 и 3/5 это будет 0.

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

Вариант 2: hashCode равен целой части от деления знаменателя на числитель.

Этот вариант подойдет для случая, когда значение дроби меньше 1. Если дробь меньше 1, значит перевернутая дробь больше 1. А если мы переворачиваем все дроби – это никак не скажется на их сравнении.

Итоговый вариант будет совмещать в себе оба решения:

public int hashCode() {
    return числитель/знаменатель + знаменатель/числитель;
}

Проверяем для дробей 2/3 и 4/6. У них должны быть равные hashCode:

Дробь 2/3 Дробь 4/6
числитель / знаменатель 2 / 3 == 0 4 / 6 == 0
знаменатель / числитель 3 / 2 == 1 6 / 4 == 1
числитель / знаменатель
+
знаменатель / числитель
0 + 1 == 1 0 + 1 == 1

На этом – все.

— Спасибо, Элли, было действительно интересно.

Комментарии (20)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Руслан Уровень 46
2 октября 2025
Ну как бы да про утилитарный класс Objects ни слова не сказано
Михаил Уровень 50
9 октября 2024
почему не написано про утилитарный класс objects
Вадим Уровень 106
26 июля 2024
Ничего не понял, спасибо alt+Ins
Олег Уровень 79 Expert
25 июня 2023
Странная реализация иквалс в первой задаче....
Юрий Кохно Уровень 62 Expert
26 марта 2023
Отдельное спасибо за объяснение значения 31 в методе hashCode
Maksym Уровень 110 Expert
25 февраля 2023
Оператор ">>>" в Java - это оператор беззнакового сдвига вправо на указанное количество бит. Например, выражение "x >>> y" выполняет беззнаковый сдвиг значения переменной x на y бит вправо, где y - целочисленное значение. При этом, в отличие от оператора ">>", которые выполняет знаковый сдвиг (сохраняя знак старшего бита), оператор ">>>" заполняет старшие биты нулями. Например, если значение переменной x равно 0b11100000 (или -32 в знаковом представлении), а значение y равно 2, то выражение "x >>> y" вернет значение 0b00111000 (или 56 в десятичной системе счисления).
Олег Уровень 79 Expert
26 июня 2023
а почему не 000b111000 ??? сдвигается же всё число, а не половина
Виктор Уровень 37
27 ноября 2022
Что означает фраза: "Должно быть обеспечено корректное поведение HashSet с типом элементов Solution"?
Gregory Parfer Уровень 82 Expert
6 января 2023
Если вникать в детали, то: HashSet - это множество уникальных значений, "обеспечение корректного поведения HashSet" значит что HashSet должен правильно работать с элементами нашего класса, то бишь не добавлять в сет неуникальные значения. Если заглянуть в его реализацию, например в метод add(E e) и пройтись по вызовам функций, то можно увидеть что проверка элемента на уникальность достигается путем методов hashCode и equals, то есть для обеспечения "корректного поведения HashSet с типом элементов Solution" нужно чтобы класс Solution правильно реализовывал методы hashCode и equals. Если еще не разобрался с этой темой, можешь посмотреть это видео, мельком глянул, вроде ничего: https://www.youtube.com/watch?v=lWnzRILIEZ0
Андрей Уровень 48
23 ноября 2022
Класно, но у нас есть уже alt+ins, минус головняк
Misha Saharin Уровень 111 Expert
19 октября 2022
ты проверил метод alt+ins? ) тсс...
Михаил Уровень 102 Expert
28 августа 2022
Решил задачи через генерацию equals и hashcode