Статьи

Работа с хэш-кодом () и равно ()

По умолчанию суперкласс Java  java.lang.Object  предоставляет два важных метода для сравнения объектов: equals () и hashcode () . Эти методы становятся очень полезными при реализации взаимодействий между несколькими классами в больших проектах. В этой статье мы поговорим об отношениях между этими методами, их реализациями по умолчанию и обстоятельствами, которые вынуждают разработчиков предоставлять индивидуальную реализацию для каждого из них.

Определение метода и реализация по умолчанию

  • equals (Object obj):  метод, предоставленный java.lang.Object, который указывает, равен ли какой-либо другой объект, переданный в качестве аргумента,текущему экземпляру. Реализация по умолчанию, предоставляемая JDK, основана на расположении памяти — два объекта равны тогда и только тогда, когда они хранятся в одном и том же адресе памяти.

  • hashcode ():  метод, предоставленный  java.lang.Object,  который возвращает целочисленное представление адреса памяти объекта. По умолчанию этот метод возвращает случайное целое число, уникальное для каждого экземпляра. Это целое число может измениться между несколькими запусками приложения и не останется прежним.

Контракт между equals () и hashcode ()

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

Согласно документации Java, разработчики должны переопределить оба метода, чтобы получить полностью работающий механизм равенства — недостаточно просто реализовать метод equals () .

Если два объекта равны в соответствии с методом equals (Object) , то вызов метода hashcode () для каждого из двух объектов должен привести к одному и тому же целочисленному результату.

В следующих разделах мы приводим несколько примеров, которые показывают важность переопределения обоих методов и недостатки переопределения equals () без hashcode () .

Практический пример

Мы определяем класс с именем Student  следующим образом:

package com.programmer.gate.beans;

public class Student {

    private int id;
    private String name;

    public Student(int id, String name) {
        this.name = name;
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

В целях тестирования мы определяем основной класс HashcodeEquals, который проверяет, считаются  ли два экземпляра объекта Student  (имеющих одинаковые атрибуты) равными.

public class HashcodeEquals {

    public static void main(String[] args) {
        Student alex1 = new Student(1, "Alex");
        Student alex2 = new Student(1, "Alex");

        System.out.println("alex1 hashcode = " + alex1.hashCode());
        System.out.println("alex2 hashcode = " + alex2.hashCode());
        System.out.println("Checking equality between alex1 and alex2 = " + alex1.equals(alex2));
    }
}

Вывод: 

alex1 hashcode = 1852704110
alex2 hashcode = 2032578917
Checking equality between alex1 and alex2 = false

Хотя два экземпляра имеют абсолютно одинаковые значения атрибутов, они хранятся в разных местах памяти. Следовательно, они не считаются равными в соответствии с реализацией equals () по умолчанию . То же самое относится и к hashcode ()  — случайный уникальный код генерируется для каждого экземпляра.

Переопределение равно ()

В бизнес-целях мы считаем, что два студента равны, если у них одинаковый идентификатор , поэтому мы переопределяем метод  equals ()  и предоставляем нашу собственную реализацию следующим образом:

@Override
public boolean equals(Object obj) {
    if (obj == null) return false;
    if (!(obj instanceof Student))
        return false;
    if (obj == this)
        return true;
    return this.getId() == ((Student) obj).getId();
}

В приведенной выше реализации мы говорим, что два ученика равны, если и только если они хранятся в одном и том же адресе памяти ИЛИ  у них одинаковый идентификатор. Теперь, если мы попытаемся запустить HashcodeEquals ,  мы получим следующий вывод:

alex1 hashcode = 2032578917
alex2 hashcode = 1531485190
Checking equality between alex1 and alex2 = true

Как вы заметили, переопределение функции equals () в  нашем настраиваемом бизнесе заставляет Java учитывать атрибут ID при сравнении двух  объектов Student .

equals () с ArrayList

Очень популярное использование equals ()   — определение списка учеников в массиве и поиск в нем конкретного ученика. Поэтому мы изменили наш класс тестирования, чтобы добиться этого.

public class HashcodeEquals {

    public static void main(String[] args) {
        Student alex = new Student(1, "Alex");

        List < Student > studentsLst = new ArrayList < Student > ();
        studentsLst.add(alex);

        System.out.println("Arraylist size = " + studentsLst.size());
        System.out.println("Arraylist contains Alex = " + studentsLst.contains(new Student(1, "Alex")));
    }
}

После выполнения вышеуказанного теста мы получаем следующий вывод:

Arraylist size = 1
Arraylist contains Alex = true

Переопределение хэш-кода ()

Итак, мы переопределяем equals ()  и получаем ожидаемое поведение — хотя хеш-код двух объектов различен. Итак, какова цель переопределения hashcode () ?

equals () с помощью HashSet

Давайте рассмотрим новый тестовый сценарий. Мы хотим сохранить всех студентов в HashSet , поэтому мы обновляем HashcodeEquals  следующим образом:

public class HashcodeEquals {

    public static void main(String[] args) {
        Student alex1 = new Student(1, "Alex");
        Student alex2 = new Student(1, "Alex");

        HashSet < Student > students = new HashSet < Student > ();
        students.add(alex1);
        students.add(alex2);

        System.out.println("HashSet size = " + students.size());
        System.out.println("HashSet contains Alex = " + students.contains(new Student(1, "Alex")));
    }
}

Если мы запустим вышеуказанный тест, мы получим следующий вывод:

HashSet size = 2
HashSet contains Alex = false

ПОДОЖДИТЕ!! Мы уже переопределяем equals ()  и проверяем, что alex1 и alex2  равны, и мы все знаем, что HashSet хранит уникальные объекты, так почему он рассматривает их как разные объекты?

HashSet хранит свои элементы в ячейках памяти. Каждое ведро связано с определенным хеш-кодом. При вызове student.add (alex1) Java хранит alex1 внутри корзины и связывает его со значением alex1.hashcode () . Теперь каждый раз, когда элемент с таким же хеш-кодом вставляется в набор, он просто заменяет alex1. Однако, поскольку у alex2 другой хэш-код, он будет храниться в отдельном сегменте и будет считаться совершенно другим объектом.

Теперь, когда HashSet ищет элемент внутри него, он сначала генерирует хеш-код элемента и ищет сегмент, соответствующий этому хеш-коду.

Здесь возникает важность переопределения hashcode () поэтому давайте переопределим его в  Student и установим его равным идентификатору, чтобы ученики с одинаковым идентификатором хранились в одном сегменте:

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

Теперь, если мы попытаемся запустить тот же тест, мы получим следующий вывод:

HashSet size = 1
HashSet contains Alex = true

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

То же самое применяется для HashMap, HashTable или любой структуры данных, которая использует механизм хеширования для хранения элементов.

Вывод

Для достижения полностью работающего пользовательского механизма равенства обязательно переопределять hashcode ()  каждый раз, когда вы переопределяете equals (). Следуйте советам ниже, и у вас никогда не будет утечек в вашем собственном механизме равенства:

  • Если два объекта равны, они ДОЛЖНЫ иметь одинаковый хэш-код.
  • Если два объекта имеют одинаковый хеш-код, это не значит, что они равны.
  • Одно только переопределение equals ()  приведет к краху вашего бизнеса с такими структурами хэширования, как: HashSet, HashMap, HashTable … и т. Д.
  • Переопределение только hashcode () не заставляет Java игнорировать адреса памяти при сравнении двух объектов.