Бесплатная миграция IT-инфраструктуры в облако

Переопределение метода equals() в Java

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

Методы equals() и hashCode() помогают сравнивать объекты. Без них пришлось бы использовать много if-ов, чтобы сравнить по отдельности поля каждого объекта. А благодаря equals() и hashCode() вы делаете код проще для чтения и понимания — никаких лишних конструкций.

Метод equals() нужен для того, чтобы сравнивать между собой объекты.В стандартной реализации он берёт один объект и сравнивает его с текущим объектом. Если ссылки на них равны, возвращается True, если не равны — возвращается False.

В свою очередь, hashCode() генерирует целочисленный код экземпляра класса. Если вы делаете переопределение equals() в Java, то необходимо переопределять hashCode(), иначе вы можете столкнуться с ошибками.

cloud

Стандартная реализация equals() 

В стандартной реализации equals() выглядит так:

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

Посмотрим на практике, как это работает. В следующем примере представлен класс User с двумя переменными nickname и rating. Мы создаём два экземпляра, передаём в них одинаковые значения и сравниваем их:

class User {
   private String nickname;
   private int rating;
   User(String nickname, int rating){
      this.nickname = nickname;
      this.rating = rating;
   }
}
public class Difference {
   public static void main(String[] args) {
     User user1 = new User("Andrew", 250);
     User user2 = new User("Andrew", 250);
      //Сравниваем два объекта и выводим результат
      boolean bool = user1.equals(user2);
      System.out.println(bool);
   }
}

По умолчанию метод equals() возвращает True, только если ссылки двух объектов равны. Поэтому программа из примера выше вернёт False — фактически ссылки у них разные. 

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

Исправить этот недостаток помогает переопределение метода equals() в Java. Суть этого механизма в изменении поведения метода equals() родительского класса в дочернем классе. Проще разобраться на примере.

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

Переопределим equals() и напишем собственную логику сравнения состояний:

class Complex {
    private double number1, number2;

    public Complex(double number1, double number2) {
        this.number1 = number1;
        this.number2 = number2;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
      }

      if (obj == null || obj.getClass() != this.getClass()) {
          return faslse;
      }

        Complex c = (Complex) obj;

        return Double.compare(number1, c.number1) == 0
                && Double.compare(number2, c.number2) == 0;
    }
}

public class Main {

    public static void main(String[] args) {
        Complex example1 = new Complex(20, 15);
        Complex example2 = new Complex(20, 15);
        if (example1.equals(example2)) {
            System.out.println("Equal ");
        } else {
            System.out.println("Not Equal ");
        }
    }
}
Output: 
Equal 

Аннотация @Override говорит компилятору, что нужно переопределить метод в процессе компиляции. Стоит учесть, что без аннотации переопределение метода также будет работать, если компилятор найдет в родительском классе метод с такой же сигнатурой. Однако наличие аннотации полезно для контроля этого действия и читаемости кода. Если же мы повесим аннотацию над методом, которого нет в родительском классе, то получим ошибку при сборке приложения.

Сам метод теперь состоит из трёх частей. Рассмотрим их подробнее.

Если объект сравнивается с самим собой, должно вернуться True:

 if (obj == this) {
            return true;
  }

Смотрим, относится ли объект к классу Complex. Возвращаем False, если это не так:

if (obj == null || obj.getClass() != this.getClass()) { 
  return faslse;
}

Приводим тип экземпляра к Complex, сравниваем элементы и возвращаем соответствие:

        Complex c = (Complex) obj;
        return Double.compare(number1, c.number1) == 0
                && Double.compare(number2, c.number2) == 0;
    }

Правила переопределения

При изменении работы метода нужно придерживаться правил переопределения equals() в Java:

  • Если объект сравнивается сам с собой, должно возвращаться True.
  • Если объект сравнивается с null, должно возвращаться False.
  • При равенстве двух объектов Obj1.equals(Obj2) и Obj2.equals(Obj1) должны возвращать True.
  • При сравнении трёх объектов Obj1.equals(Obj2) и Obj2.equals(Obj3) возвращают True, то и Obj1.equals(Obj3) должно вернуть True.
  • При многократных вызовах метода должен возвращаться один и тот же результат, пока не изменятся свойства объекта, используемые в вашей реализации.

Существуют также некоторые ограничения на переопределение equals(). Например, переопределять метод нет смысла, если каждый объект уникален. Кроме того, это относится к классам, которые предназначены не для работы с данными, а для предоставления определённого поведения.

Ещё одна ситуация, когда метод не переопределяют, — использование класса, экземпляры которого сравнивать бессмысленно. Наглядный пример — java.util.Random. Суть этого  класса в том, чтобы возвращать случайные последовательности чисел. Экземпляры этого класса не должны быть равными, иначе в них нет смысла.

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

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

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

Плохой пример переопределения hashCode() — возврат константы. Например, вот так:

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

На практике это создаёт огромные проблемы. Хэш-значение не будет меняться при изменении состояния. Допустим, вы измените значения полей. Хэш-код останется прежним.

В определении хэш-значения должны принимать участие только те поля, которые используются в equals(). Кроме того, нужна база — основу для вычисления хэша. Обычно базой делают число 31, но вы можете установить любое другое значение.

Правила вычисления:

  • Переменной result присваивается ненулевое значение — например, число 31.
  • Для каждого значимого поля экземпляра вычисляется хэш. Правила вычислений отличаются в зависимости от типа поля:
    • для boolean — (f ? 1 : 0);
    • для byte, char, short или int — (int) f;
    • для long — (int)(f ^ (f >>> 32));
    • для float — Float.floatToIntBits(f);
    • для double — Double.doubleToLongBits(f), а затем как с long;
    • для полей, которые представляют собой ссылку на другой объект — рекурсивный вызов hashCode();
    • для null — вернуть 0;
    • для массива — обработайте так, будто каждый элемент представляет собой отдельное поле объекта.

После обработки каждого поля вы должны прибавлять полученный результат к базе и предыдущим результатам. После прохождения по всем полям верните итоговый хэш-код.

Допустим, вы хотите переопределить hashCode() для класса Person:

public class Person {
   private int age;
    private int number;
    private double salary;
    private String name;
    private CarKey carKey;

    public Person(int age, int number, String name, double salary, CarKey carKey) {
        this.age = age;
        this.number = number;
        this.name = name;
        this.salary = salary;
        this.carKey = carKey;
    }

    @Override
    public int hashCode() {
      int result = 31;
      result = result * 17 + age;
      result = result * 17 + number;
      long lnum = Double.doubleToLongBits(salary);
      result = result * 17 + (int)(lnum ^ (lnum >>> 32));
      result = result * 17 + name.hashCode();
      result = result * 17 + carKey.hashCode();
      return result;
    }
    // Здесь уже можно переопределить equals()
    // …
}

Начиная с Java 7 доступы вспомогательные методы для создания собственной реализации hashCode(). Например, для того же класса Person достаточно выполнить:

@Override
public int hashCode() {
    return Objects.hash(age, number, salary, name, carKey);
}

Строгие правила переопределения разработаны и для hashCode():

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

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

Подготовили для вас выгодные тарифы на облачные серверы

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

  • Метод equals() можно переопределить так, чтобы он сравнивал значения полей, сопоставляя между собой состояния экземпляров.
  • Если сравнение двух хэш-кодов даёт False, то и результат выполнения equals() должен возвращать False.
  • Если вы создаёте собственную реализацию equals(), то измените реализацию hashCode().
  • Если при использовании коллекций, использующих хэш-таблицы, не переопределить оба метода, то в коллекции могут быть повторяющиеся элементы.
Хотите внести свой вклад?
Участвуйте в нашей контент-программе за
вознаграждение или запросите нужную вам инструкцию
img-server
09 сентября 2022 г.
4516
9 минут чтения
Средний рейтинг статьи: 5
Комментарии 2
Chybaka1337
Chybaka1337
01.06.2024, 16:26
     @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }

        if (obj.getClass() != this.getClass()) { // <- При передаче аргумента null будет 
            return false;                        // NullPointerException из-за попытки 
                                                 // разыменовать null и вызвать getClass() . 
        }

        Complex c = (Complex) obj;

        return Double.compare(number1, c.number1) == 0
                && Double.compare(number2, c.number2) == 0;
    }
}


// Измененная реализация, где не будет исключений при передаче null в метод
if (obj == null || obj.getClass() != this.getClass()) {
    return faslse;
}
Команда Timeweb Cloud
Команда Timeweb Cloud
03.06.2024, 10:58

Хорошее уточнение, спасибо! Обновили код в статье ✍️