Метод 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():
- Проверка ссылок
this == o. Если это один и тот же объект, дальше можно не проверять — сразуtrue. Это быстрая оптимизация. - Проверка на
nullи класс. По контрактуx.equals(null)всегда возвращаетfalse. СравнениеgetClass()гарантирует, что объект другого класса (в том числе подкласса) не будет равенPerson. - Приведение типа к
Person— теперь оно безопасно. - Сравнение значимых полей. Примитивы (
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 На чём чаще всего ошибаются
- Перегрузка вместо переопределения. Метод
public boolean equals(Person p)не переопределяетequals(Object), а создаёт новый перегруженный метод. Коллекции вызываютequals(Object), поэтомуlist.contains(person)будет сравнивать ссылки. Аннотация@Overrideпревращает такую опечатку в ошибку компиляции — ставьте её всегда. - Вызов
equals()уnull.str.equals("abc")упадёт сNullPointerException, еслиstr == null. Используйте"abc".equals(str)илиObjects.equals(str, "abc"). - Сравнение строк через
==. Иногда это «работает» благодаря пулу строк (литералы интернируются), но строки, полученные изnew String(), конкатенации во время выполнения,Scannerили базы данных, — отдельные объекты. - Ожидание, что
equals()сравнит массивы. Массивы не переопределяютequals():arr1.equals(arr2)сравнивает ссылки. Для содержимого нуженArrays.equals(arr1, arr2), для многомерных —Arrays.deepEquals(). StringBuilderиStringBuffer. Они тоже не переопределяютequals(). Сравнивайтеsb1.toString().equals(sb2.toString())или, начиная с Java 11,sb1.compareTo(sb2) == 0.BigDecimalучитывает масштаб.new BigDecimal("1.0").equals(new BigDecimal("1.00"))возвращаетfalse, потому что у чисел разное количество знаков после запятой. Для сравнения значений используйтеcompareTo(...) == 0.- Поля
doubleиfloatчерез==. Внутриequals()их лучше сравнивать черезDouble.compare(a, b) == 0(илиFloat.compare): так корректно обрабатываютсяNaNи различие между0.0и-0.0, и результат согласуется сDouble.hashCode(). - Переопределён
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(), поэтому они попадают в разные корзины. Правило простое: переопределяете один метод — переопределяйте и второй по тем же полям.
Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии