Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Равенство и хеш-коды

В 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
true

equals принимает 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 вместе — или используйте записи, которые сделают это за вас.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.