Равенство и хеш-коды
В Java есть два вида равенства. == проверяет, что две переменные указывают на один и тот же объект в памяти. Метод equals проверяет, что объекты равны по смыслу — например, две строки состоят из одних и тех же символов. Для коллекций вроде HashSet и HashMap важен ещё и хеш-код — число, которое объект выдаёт методом hashCode().
== и equals
Создадим две одинаковые строки. Это два разных объекта с одинаковым содержимым:
void main() {
String a = new String("кот");
String b = new String("кот");
IO.println(a == b);
IO.println(a.equals(b));
}Результат:
false
true== вернул false: объекты разные. equals вернул true: содержимое совпадает. Поэтому строки и другие объекты всегда сравнивают через equals, а == оставляют для примитивов вроде int.
equals по умолчанию
Если класс не переопределил equals, он наследует версию из Object, которая работает как ==. Два «одинаковых» объекта считаются разными:
class Point {
final int x;
final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
void main() {
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
IO.println(p1.equals(p2));
}Результат:
falseСвой equals и hashCode
Чтобы объекты сравнивались по полям, переопределяют equals. Вместе с ним обязательно переопределяют hashCode. Правило (контракт) такое: если два объекта равны по equals, их hashCode должны совпадать. Удобные помощники — Objects.equals (безопасно сравнивает даже null) и Objects.hash:
class Point {
final int x;
final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point other)) return false;
return x == other.x && y == other.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
void main() {
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
IO.println(p1.equals(p2));
IO.println(p1.hashCode() == p2.hashCode());
}Результат:
true
trueequals принимает Object, поэтому сначала проверяем тип — здесь пригодился шаблон в instanceof.
Зачем нужен hashCode
HashSet и HashMap раскладывают объекты по «корзинам» по их хеш-коду. Чтобы найти объект, коллекция сначала вычисляет хеш-код, идёт в нужную корзину и только там сравнивает через equals. Поэтому хеш-коды равных объектов обязаны совпадать — иначе коллекция будет искать не в той корзине. А вот разные объекты могут случайно иметь одинаковый хеш-код — это нормально:
void main() {
IO.println("Aa".hashCode());
IO.println("BB".hashCode());
IO.println("Aa".equals("BB"));
}Результат:
2112
2112
falseЗаписи делают это сами
У записей equals, hashCode и toString создаются автоматически по всем компонентам. Поэтому записи — лучший выбор для объектов, которые сравнивают по значению:
record Point(int x, int y) {}
void main() {
Set<Point> visited = new HashSet<>();
visited.add(new Point(1, 2));
visited.add(new Point(1, 2));
visited.add(new Point(3, 4));
IO.println(visited.size());
IO.println(visited.contains(new Point(3, 4)));
}Результат:
2
trueЧастая ошибка
Если переопределить только equals и забыть hashCode, объекты равны, но у них разные хеш-коды. HashSet положит их в разные корзины и будет считать разными:
class Point {
final int x;
final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
return o instanceof Point other && x == other.x && y == other.y;
}
}
void main() {
Set<Point> points = new HashSet<>();
points.add(new Point(1, 2));
points.add(new Point(1, 2));
IO.println(new Point(1, 2).equals(new Point(1, 2)));
IO.println(points.size());
IO.println(points.contains(new Point(1, 2)));
}Результат:
true
2
falseОшибки компиляции нет, но коллекция ведёт себя неправильно. Всегда переопределяйте equals и hashCode вместе — или используйте записи, которые сделают это за вас.