Перейти к содержимому
Теория · Обёртки и равенство · урок 7 из 8

equals в своём классе

У строк и обёрток equals сравнивает содержимое: символы строки, число внутри Integer. А что он сделает с объектами вашего класса? Возьмём знакомую точку Point и сравним её с другой точкой с теми же координатами:

JAVA
public class Main {
    public static void main(String[] args) {
        Point a = new Point(1, 2);
        System.out.println(a.equals(new Point(1, 2))); // false
    }
}

class Point {
    int x;
    int y;
    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Точка a — это (1, 2), и с ней сравнивается новая точка (1, 2). Координаты совпадают, а equals отвечает false.

Вернёмся к конвертам. У Integer метод equals вскрывает оба конверта и сравнивает монеты. А equals вашего класса не знает, что лежит внутри. Он смотрит только на сами конверты: один это конверт или два. Здесь картинка неточна: в конверте Point лежат сразу две монеты, x и y, и что значит «равны» для двух точек, решает сам класс.

equals из Object

Метод equals есть у каждого объекта. Класс Point получил его от Object, корня всех классов, как вы видели в теме о наследовании. В Object этот метод возвращает this == obj, то есть сравнивает ссылки.

Поэтому, пока класс не переопределил equals, для его объектов a.equals(b) значит то же, что a == b. String и Integer переопределяют equals и сравнивают содержимое. Так может и ваш класс.

Что делает equals, если его не переопределить, — частый вопрос на собеседовании. Ответ: сравнивает ссылки, как ==.

Свой equals

Научим Point сравнивать координаты. Для этого в класс добавляют метод equals, который заменяет метод из Object:

JAVA
class Point {
    int x;
    int y;
    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    @Override
    public boolean equals(Object o) {
        return o instanceof Point p && x == p.x && y == p.y;
    }
}

Давайте разберём новый метод по частям:

  1. @Override просит компилятор проверить, что метод действительно заменяет метод из Object.
  2. Заголовок public boolean equals(Object o) повторяет заголовок из Object в точности: параметр типа Object, а не Point.
  3. o instanceof Point p проверяет, что другой объект — точка, и сразу даёт ему имя p типа Point. Для null и для объекта другого класса проверка даёт false.
  4. x == p.x && y == p.y сравнивает поля. Благодаря сокращённому вычислению && эта часть выполняется, только если o — точка.

С таким классом первая программа печатает true, а сравнение с new Point(2, 1) даёт false. Запись record пишет такой equals сама, по всем компонентам, а обычный класс приходится учить вручную.

Обычный javac компилирует этот класс молча, а javac -Xlint:all предупреждает: Class Point overrides equals, but neither it nor any superclass overrides hashCode method. У equals есть напарник — метод hashCode, и в рабочем коде их переопределяют только вместе. Наш Point пока обходится без него: зачем нужен hashCode, разберём в теме о коллекциях.

Поля-объекты и Objects.equals

В Point оба поля — числа, и их сравнивает ==. Поле ссылочного типа, например String name, так сравнивать нельзя: == сравнит ссылки. А вызов name.equals(…) бросит NullPointerException, если в name лежит null. Для полей-объектов есть метод Objects.equals:

JAVA
String a = null;
String b = null;
System.out.println(Objects.equals(a, b));           // true
System.out.println(Objects.equals(a, "Anna"));      // false
System.out.println(Objects.equals("Anna", "Anna")); // true

Objects.equals(a, b) работает так:

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

Класс Objects лежит в пакете java.util, поэтому в начале файла нужна строка import java.util.Objects;. Внутри своего equals поле-примитив сравнивают через ==, а поле-объект — через Objects.equals. Например, если в классе User есть поле String name, а другой объект получил имя u в проверке o instanceof User u, это Objects.equals(name, u.name).

Частая ошибка

В заголовке equals написали тип своего класса и не поставили @Override:

JAVA
class Point {
    int x;
    int y;
    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    public boolean equals(Point p) { // ✗
        return x == p.x && y == p.y;
    }
}

Такой класс компилируется, и a.equals(b) для двух переменных типа Point даже даёт true. Но это не замена метода из Object, а перегрузка: у класса теперь два equals, с параметром Object и с параметром Point. Код, который видит в точке только Object, вызывает старый метод, поэтому Objects.equals(a, b) даёт false. С @Override компилятор сразу поймал бы ошибку:

TEXT
error: method does not override or implement a method from a supertype

Параметр у equals всегда типа Object, а @Override над методом защищает от ошибки в заголовке.

Главное

  • Без переопределения equals из Object сравнивает ссылки, как ==, поэтому две точки (1, 2) не равны.
  • Свой equals пишут с заголовком public boolean equals(Object o) и @Override; o instanceof Point p отсекает null и объекты других классов.
  • Поля-примитивы сравнивают через ==, а поля-объекты — через Objects.equals, который не боится null.
  • Вместе с equals всегда переопределяют hashCode.