Что будет, если переопределить equals() не переопределяя hashCode()? Какие могут возникнуть проблемы?

Вопрос с ответом из телеграмм паблика, на который подписан.

Вопрос: Что будет, если переопределить equals() не переопределяя hashCode()? Какие могут возникнуть проблемы?

Ответ: Классы и методы, которые используют правила этого контракта могут работать некорректно. Так для HashMap это может привести к тому, что пара «ключ-значение», которая была в неё помещена при использовании нового экземпляра ключа не будет в ней найдена.

Меня смутил ответ. Я не смог представить вариант, при котором бы сработал данный сценарий. Ведь мы не меняем алгоритм работы хэша. Значит выбор корзины всегда будет верный. Но если мы выбрали правильную корзину, то уже без проблем получим искомый ключ. Даже если будет нарушен контракт между хэш-кодом и иквелсом, то это просто приведет к тому, два одинаковых по иквелсу ключа имеющие разные хэши просто будут не затирать друг друга, а лежать в разных корзинах. Что теоретически хоть и нарушит логику работы (2 одинаковых ключа с разными значениями), но технически будет "рабочим" вариантом.


Ответы (4 шт):

Автор решения: Ada

Вы пишете, что так как мы не меняем алгоритм работы хэша, то выбор корзины всегда будет верный. И тут возникает вопрос. Верный относительно чего? Ведь если вы переопределяете equals, то вы говорите, что ваши объекты будут сравниваться не так, как вы их сравнивали раньше. Поэтому вам также нужно переопределить hashCode

→ Ссылка
Автор решения: CrazyElf

Я в Java не очень понимаю, но немного почитав материалы могу так сказать.

Ведь мы не меняем алгоритм работы хэша. Значит выбор корзины всегда будет верный.

Нет, например у объектов типа object метод hashCode вроде как выдаёт рандомное значение. Поэтому корзина может быть неправильная. Если вы расширяете какой-то другой класс, с нормально определённым hashCode, то да, скорее всего корзина будет правильная, но в общем случае это не верно.

Даже если будет нарушен контракт между хэш-кодом и иквелсом, то это просто приведет к тому, два одинаковых по иквелсу ключа имеющие разные хэши просто будут не затирать друг друга, а лежать в разных корзинах. Что теоретически хоть и нарушит логику работы (2 одинаковых ключа с разными значениями), но технически будет "рабочим" вариантом.

Если корзина будет получаться разная, то вы уже не сможете найти объект в хэш-таблице, если создадите другой точно такой же объект и попытаетесь воспользоваться им как ключом. Какой же это "рабочий" вариант, если вы кладёте объект в хэш-таблицу, а потом не можете его там найти?

→ Ссылка
Автор решения: had0uken
  1. Что поменяется: процесс сравнения объектов станет намного медленней и неэффективным. При сравнении элементов в 1ую очередь сравнивается их hash (это очень быстро, просто сравнить два числа), и только в случае равенства хэшей запускается метод equals (он намного медленней, но если хэш функция нормально прописана - то запуск equals при неравенстве объектов - крайне редкое явление - называется "коллизия").
  2. Некоторые коллекции используют хэширование при поиске и сравнении (HashMap, HashSet). И если, допустим имеется класс Car и HashMap, в котором ключ это объект класса Car, в котором не переопределен метод ХэшКод. Вы не сможете нормально искать объекты в коллекции. Метод contains вам будет возвращать false при поиске объекта.
public class Main {
    public static void main(String[] args) {
        Car car1 = new Car("Blue");
        Car car2 = new Car("Black");
        Car car3 = new Car("Red");

        Map<Car, Integer> map = new HashMap<>();
        map.put(car1, 1);
        map.put(car2, 2);
        map.put(car3, 3);

        Car car4 = new Car("Black");
        System.out.println(car4.equals(car2));
        System.out.println(map.containsKey(car4));
    }
}

class Car {
    private String color;

    public Car(String color) {
        this.color = color;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Car car = (Car) o;
        return Objects.equals(color, car.color);
    }
}

Выход:

true
false

Но как только вы переопределите метод hashcode в классе Car

    @Override
    public int hashCode() {
        return Objects.hash(color);
    }
}

Ситуация изменится и вы будете получать выход:

true
true

Поэтому рекомендуется при написании класса переопределять и hashCode() и equals().

→ Ссылка
Автор решения: had0uken

Демонстрация очередности работы методов equals и хэшкод (ответ на комментарий)

public class Main {
    public static void main(String[] args) {
        Car car1 = new Car("Blue");
        Car car2 = new Car("Black");
        Car car3 = new Car("Red");
        Map<Car, Integer> map = new HashMap<>();
        map.put(car1, 1);
        map.put(car2, 2);
        map.put(car3, 3);
        System.out.println("Before creating car 4");
        Car car4 = new Car("Black");
        System.out.println("Before creating car 5");
        Car car5 = new Car("Yellow");
        //System.out.println(car4.equals(car2));
        System.out.println("Before checking car 4");
        System.out.println(map.containsKey(car4));

        System.out.println("Before checking car 5");
        System.out.println(map.containsKey(car5));
    }
}
class Car {
    private String color;
    public Car(String color) {
        this.color = color;
    }

    @Override
    public boolean equals(Object o) {
        System.out.println("Enter to equals method");
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Car car = (Car) o;
        return Objects.equals(color, car.color);
    }

    @Override
    public int hashCode() {
        System.out.println("Enter to hashCode method");
        return Objects.hash(color);
    }
}

Выход:

Enter to hashCode method
Enter to hashCode method
Enter to hashCode method
Before creating car 4
Before creating car 5
Before checking car 4
Enter to hashCode method
Enter to equals method
true
Before checking car 5
Enter to hashCode method
false

Process finished with exit code 0
→ Ссылка