Object ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-09-28

Метод equals() в Java: сравнение объектов и переопределение

Два объекта с одинаковыми данными могут оказаться «не равны»: new String("Java") == new String("Java") вернёт false, хотя обе строки содержат одно и то же слово. Всё дело в том, что == сравнивает ссылки, а не содержимое, и для сравнения объектов в Java нужен метод equals().

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

Как работает Object.equals() по умолчанию

Каждый класс в Java неявно наследуется от Object, поэтому метод equals() есть у любого объекта. Его исходная реализация в JDK выглядит так:

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

То есть, пока класс не переопределил equals(), этот метод ведёт себя точно так же, как операция ==: возвращает true, только если обе ссылки указывают на один и тот же объект в памяти.

Важно

Часто пишут, что «equals() сравнивает содержимое объектов». Это верно только для классов, где метод переопределён: String, классы-обёртки (Integer, Double…), коллекции, LocalDate и т.д. Для вашего собственного класса без переопределения equals() сравнивает ссылки, как ==.

Классы стандартной библиотеки уже переопределяют equals(), поэтому для них сравнение по содержимому работает «из коробки»:

String s1 = new String("Java");
String s2 = new String("Java");
System.out.println(s1 == s2);      // false - разные объекты
System.out.println(s1.equals(s2)); // true  - одинаковые символы

List<Integer> list1 = List.of(1, 2, 3);
List<Integer> list2 = new ArrayList<>(List.of(1, 2, 3));
System.out.println(list1.equals(list2)); // true - те же элементы в том же порядке

Разница между == и equals() в Java

Операция == для ссылочных типов проверяет идентичность (один ли это объект), а equals() — логическое равенство (одинаковы ли объекты по смыслу). Для примитивов (int, double, boolean…) используется только ==: методов у них нет, и сравниваются сами значения.

Способ Что сравнивает Реакция на null Когда использовать
a == b Для примитивов — значения, для объектов — ссылки Безопасно: null == null даёт true Примитивы, enum, проверка «это тот же самый объект?»
a.equals(b) Логическое равенство (как определено в классе a) NullPointerException, если a == null; a.equals(null) даёт false Сравнение объектов, когда a гарантированно не null
Objects.equals(a, b) То же, что a.equals(b), плюс корректная обработка null Безопасно: оба null — true, один null — false Сравнение объектов, любой из которых может быть null

Поэтому для сравнения объектов по содержимому (строк, дат, чисел-обёрток, сущностей) используйте equals() или Objects.equals(), а == оставьте для примитивов, констант enum и тех случаев, когда нужно проверить именно совпадение ссылок.

Как переопределить equals(): пример

Для собственного класса логическое равенство определяете вы сами: решаете, какие поля делают два объекта «одинаковыми». Рассмотрим класс Person с тремя полями: fullName, age и retired. В переопределённом методе equals() участвуют все три. Если какое-то поле не должно влиять на равенство (например, технический счётчик или кэш), его можно не проверять — но тогда его нельзя использовать и в hashCode().

import java.util.Objects;

public class Person {
    private final String fullName;
    private final int age;
    private final boolean retired;

    public Person(String fullName, int age, boolean retired) {
        this.fullName = fullName;
        this.age = age;
        this.retired = retired;
    }

    public String getFullName() {
        return fullName;
    }

    public int getAge() {
        return age;
    }

    public boolean isRetired() {
        return retired;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {                                  // 1. та же ссылка
            return true;
        }
        if (o == null || getClass() != o.getClass()) {    // 2. null или другой класс
            return false;
        }
        Person person = (Person) o;                       // 3. приведение типа
        return age == person.age                          // 4. сравнение полей
                && retired == person.retired
                && Objects.equals(fullName, person.fullName);
    }

    @Override
    public int hashCode() {
        return Objects.hash(fullName, age, retired);
    }
}

Разберём шаги метода equals():

  1. Проверка ссылок this == o. Если это один и тот же объект, дальше можно не проверять — сразу true. Это быстрая оптимизация.
  2. Проверка на null и класс. По контракту x.equals(null) всегда возвращает false. Сравнение getClass() гарантирует, что объект другого класса (в том числе подкласса) не будет равен Person.
  3. Приведение типа к Person — теперь оно безопасно.
  4. Сравнение значимых полей. Примитивы (int, boolean) сравниваются через ==, ссылочные поля — через Objects.equals(), который корректно обрабатывает случай, когда fullName равен null. Это короткая запись для конструкции fullName != null ? fullName.equals(person.fullName) : person.fullName == null.

Писать такой код вручную не обязательно: IntelliJ IDEA генерирует equals() и hashCode() через Code → Generate (Alt+Insert), Eclipse — через Source → Generate hashCode() and equals().

Теперь создадим два объекта person1 и person2 с одинаковыми значениями полей и третью переменную person3, которой присвоим ссылку из person1:

public class PersonExample2 {
    public static void main(String[] args) {
        Person person1 = new Person("Петров Иван Иванович", 56, false);
        Person person2 = new Person("Петров Иван Иванович", 56, false);
        Person person3 = person1;

        System.out.println("person1 == person2? " + (person1 == person2));
        System.out.println("person1 == person3? " + (person1 == person3));

        System.out.println("person1.equals(person2)? " + person1.equals(person2));
        System.out.println("person1.equals(person3)? " + person1.equals(person3));
    }
}

Результат выполнения:

person1 == person2? false
person1 == person3? true
person1.equals(person2)? true
person1.equals(person3)? true

Что здесь происходит:

  • person1 == person2 — false: это два разных объекта, хотя данные в них одинаковые.
  • person1 == person3 — true: обе переменные ссылаются на один объект.
  • person1.equals(person2) — true: переопределённый метод сравнивает поля, а они совпадают.
  • person1.equals(person3) — true: срабатывает первая же проверка this == o.

Если убрать из класса Person переопределённый equals(), строка person1.equals(person2) тоже выведет false — сработает реализация из Object, сравнивающая ссылки.

Контракт метода equals()

Документация класса Object требует, чтобы любая реализация equals() для ненулевых ссылок x, y, z соблюдала пять свойств. Коллекции (HashMap, HashSet, ArrayList.contains() и др.) полагаются на них, и нарушение приводит к трудноуловимым багам.

Свойство Правило Что означает на практике
Рефлексивность x.equals(x) всегда true Объект равен самому себе
Симметричность x.equals(y) == y.equals(x) Порядок сравнения не влияет на результат
Транзитивность Если x.equals(y) и y.equals(z), то x.equals(z) Равенство «передаётся» по цепочке
Согласованность Повторные вызовы x.equals(y) дают один и тот же результат Пока поля, участвующие в сравнении, не меняются
Сравнение с null x.equals(null) всегда false Никакого NullPointerException внутри equals()

equals() и hashCode()

Контракт equals() неразрывно связан с методом hashCode(): если два объекта равны по equals(), у них обязан совпадать hashCode(). Обратное не требуется — у разных объектов хеш-коды могут совпасть (коллизия).

Хеш-коллекции (HashMap, HashSet) сначала находят «корзину» по hashCode() и только потом вызывают equals(). Если переопределить equals(), но оставить hashCode() из Object, два логически равных объекта почти наверняка попадут в разные корзины, и коллекция их «не увидит». Поэтому оба метода переопределяют вместе и по одному и тому же набору полей — как в примере Person выше, где используется Objects.hash(...).

Совет

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

Objects.equals(): сравнение с учётом null

Вызов a.equals(b) бросает NullPointerException, если a равен null. Чтобы не писать проверки вручную, в Java 7 появился утилитный метод java.util.Objects.equals(Object a, Object b). Он работает так:

  • если a и b — одна ссылка (в том числе оба null), возвращает true;
  • если ровно один из аргументов null, возвращает false;
  • иначе возвращает результат a.equals(b).
String name = null;

// System.out.println(name.equals("Иван")); // NullPointerException!
System.out.println("Иван".equals(name));          // false - литерал слева, NPE нет
System.out.println(Objects.equals(name, "Иван")); // false
System.out.println(Objects.equals(null, null));   // true

Приём с константой слева ("Иван".equals(name)) тоже защищает от NullPointerException, но подходит, только когда одна из сторон заведомо не null. Objects.equals() универсален и поэтому рекомендуется для сравнения полей внутри собственного equals(). Для массивов есть отдельный метод Objects.deepEquals(), который сравнивает содержимое, включая вложенные массивы.

getClass() или instanceof, pattern matching и record

Для проверки типа в equals() применяют два подхода:

  • getClass() != o.getClass() — объект равен только экземпляру точно такого же класса. Подкласс никогда не будет равен родителю, зато симметричность гарантирована. Этот вариант генерирует IntelliJ IDEA по умолчанию.
  • o instanceof Person — подклассы тоже проходят проверку. Этот вариант естествен для final-классов и интерфейсных иерархий, но опасен, если подкласс добавляет свои поля и переопределяет equals(): тогда person.equals(employee) может вернуть true, а employee.equals(person) — false, то есть нарушится симметричность.

Начиная с Java 16, проверку instanceof и приведение типа можно объединить с помощью pattern matching — код становится короче:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Person other)) return false; // проверка + приведение; null даёт false
    return age == other.age
            && retired == other.retired
            && Objects.equals(fullName, other.fullName);
}

Отдельная проверка на null здесь не нужна: null instanceof Person всегда false.

Если класс — простой носитель данных, удобнее объявить его как record (Java 16+). Компилятор сам сгенерирует equals(), hashCode() и toString() по всем компонентам:

public record PersonRecord(String fullName, int age, boolean retired) { }

PersonRecord r1 = new PersonRecord("Петров Иван Иванович", 56, false);
PersonRecord r2 = new PersonRecord("Петров Иван Иванович", 56, false);
System.out.println(r1.equals(r2)); // true
System.out.println(r1 == r2);      // false

На чём чаще всего ошибаются

  1. Перегрузка вместо переопределения. Метод public boolean equals(Person p) не переопределяет equals(Object), а создаёт новый перегруженный метод. Коллекции вызывают equals(Object), поэтому list.contains(person) будет сравнивать ссылки. Аннотация @Override превращает такую опечатку в ошибку компиляции — ставьте её всегда.
  2. Вызов equals() у null. str.equals("abc") упадёт с NullPointerException, если str == null. Используйте "abc".equals(str) или Objects.equals(str, "abc").
  3. Сравнение строк через ==. Иногда это «работает» благодаря пулу строк (литералы интернируются), но строки, полученные из new String(), конкатенации во время выполнения, Scanner или базы данных, — отдельные объекты.
  4. Ожидание, что equals() сравнит массивы. Массивы не переопределяют equals(): arr1.equals(arr2) сравнивает ссылки. Для содержимого нужен Arrays.equals(arr1, arr2), для многомерных — Arrays.deepEquals().
  5. StringBuilder и StringBuffer. Они тоже не переопределяют equals(). Сравнивайте sb1.toString().equals(sb2.toString()) или, начиная с Java 11, sb1.compareTo(sb2) == 0.
  6. BigDecimal учитывает масштаб. new BigDecimal("1.0").equals(new BigDecimal("1.00")) возвращает false, потому что у чисел разное количество знаков после запятой. Для сравнения значений используйте compareTo(...) == 0.
  7. Поля double и float через ==. Внутри equals() их лучше сравнивать через Double.compare(a, b) == 0 (или Float.compare): так корректно обрабатываются NaN и различие между 0.0 и -0.0, и результат согласуется с Double.hashCode().
  8. Переопределён equals(), но забыт hashCode(). Объекты «теряются» в HashSet и HashMap — подробнее в разделе equals() и hashCode().

Часто задаваемые вопросы

Можно ли сравнивать строки в Java через ==?

Нельзя, если нужно сравнить текст. Операция == проверяет, указывают ли переменные на один объект. Два строковых литерала "Java" обычно совпадают по ссылке благодаря пулу строк, но строка из new String("Java"), пользовательского ввода или конкатенации в рантайме будет другим объектом, и == вернёт false. Для строк всегда используйте equals() или equalsIgnoreCase(), если регистр не важен.

Почему Integer 127 == 127 даёт true, а 128 == 128 — false?

При автоупаковке Integer a = 127 вызывается Integer.valueOf(), который по умолчанию кэширует объекты для значений от -128 до 127. Поэтому для 127 обе переменные ссылаются на один закэшированный объект, а для 128 создаются два разных, и == возвращает false. Объекты-обёртки (Integer, Long и т.д.) сравнивайте через equals() или распаковывайте в примитивы.

Нужно ли переопределять equals() для enum и record?

Нет. В enum метод equals() объявлен как final и сравнивает ссылки, а каждая константа существует в единственном экземпляре, поэтому константы enum можно безопасно сравнивать через == (это ещё и защищает от NullPointerException). Для record компилятор сам генерирует equals() и hashCode() по всем компонентам; переопределять их имеет смысл, только если нужна особая логика, например сравнение массива-компонента по содержимому.

Как сравнить два массива в Java?

Метод equals() у массива унаследован от Object и сравнивает ссылки, поэтому new int[]{1, 2}.equals(new int[]{1, 2}) вернёт false. Для сравнения содержимого используйте Arrays.equals(a, b), а для многомерных массивов — Arrays.deepEquals(a, b).

Что будет, если переопределить equals(), но не hashCode()?

Код скомпилируется, но хеш-коллекции начнут работать неправильно. Например, в HashSet можно добавить два равных по equals() объекта Person, и оба там окажутся, а set.contains(new Person(...)) вернёт false для уже добавленных данных. Причина: у равных объектов разные хеш-коды из Object.hashCode(), поэтому они попадают в разные корзины. Правило простое: переопределяете один метод — переопределяйте и второй по тем же полям.

Видео объяснение

Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.

Комментарии

liftvaffe Jun 25, 2022
Ура!!Заработало!!

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.