equals в своём классе
У строк и обёрток equals сравнивает содержимое: символы строки, число внутри Integer. А что он сделает с объектами вашего класса? Возьмём знакомую точку Point и сравним её с другой точкой с теми же координатами:
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:
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;
}
}Давайте разберём новый метод по частям:
@Overrideпросит компилятор проверить, что метод действительно заменяет метод изObject.- Заголовок
public boolean equals(Object o)повторяет заголовок изObjectв точности: параметр типаObject, а неPoint. o instanceof Point pпроверяет, что другой объект — точка, и сразу даёт ему имяpтипаPoint. Дляnullи для объекта другого класса проверка даётfalse.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:
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")); // trueObjects.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:
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 компилятор сразу поймал бы ошибку:
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.