JavaRush /Курси /JAVA 25 SELF /equals, hashCode, toString: автоматична генерація

equals, hashCode, toString: автоматична генерація

JAVA 25 SELF
Рівень 22 , Лекція 2
Відкрита

1. Автоматична генерація equals, hashCode, toString

Навіщо потрібні ці методи?

Працюючи з об’єктами у Java, ви доволі швидко стикаєтеся з одними й тими самими завданнями. Іноді потрібно перевірити, чи рівні два об’єкти. Наприклад, зрозуміти, чи є він уже в колекції на кшталт Set або Map. В інших випадках об’єкт використовується як ключ у HashMap, і тут без спеціальних правил порівняння ніяк. А ще часто хочеться надрукувати об’єкт у журналі або на екрані так, щоб виведення було не просто абракадаброю на кшталт MyClass@7b23ec81, а чимось осмисленим.

Саме для цього кожен клас у Java має три спеціальні методи:

  • equals(Object o) відповідає за перевірку рівності.
  • hashCode() повертає числовий «відбиток» об’єкта, потрібний колекціям на кшталт хеш‑таблиць.
  • toString() повертає зручне рядкове подання об’єкта, що суттєво спрощує налагодження та друк.

Чому це незручно у звичайних класах?

У звичайних класах ці методи доводиться писати вручну. І тут починається рутина й головний біль. Накопичується купа шаблонного коду, який лише захаращує клас. Дуже легко десь помилитися: забути порівняти поле, неправильно обчислити hashCode і потім відстежувати загадкові баги. А якщо до класу додати нове поле — доведеться знову лізти в усі ці методи й переписувати код.

Приклад звичайного класу


public class Point {
    private final int x;
    private final int y;

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

    public int x() { return x; }
    public int y() { return y; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }

    @Override
    public int hashCode() {
        return 31 * x + y;
    }

    @Override
    public String toString() {
        return "Point[x=" + x + ", y=" + y + "]";
    }
}

Виглядає знайомо? Так, і це лише для двох полів! А якщо їх двадцять?

Як це робить record

Record‑клас робить усе це за вас. Просто оголосіть:


public record Point(int x, int y) { }

І Java сама згенерує:

  • Конструктор
  • Гетери (x(), y())
  • equals, hashCode, toString

Автоматично згенеровані методи

  • equals порівнює всі компоненти record за значенням.
  • hashCode обчислюється за всіма компонентами.
  • toString повертає рядок вигляду Point[x=1, y=2].

Подивімося наживо!


public record Point(int x, int y) {}

public class Demo {
    public static void main(String[] args) {
        Point p1 = new Point(1, 2);
        Point p2 = new Point(1, 2);

        System.out.println(p1.equals(p2)); // true
        System.out.println(p1.hashCode() == p2.hashCode()); // true
        System.out.println(p1); // Point[x=1, y=2]
    }
}

Виведення:

true
true
Point[x=1, y=2]

Усе працює, як і очікувалося, без жодного зайвого рядка коду!

2. Чому це важливо: колекції, налагодження та безпека

Коректна робота в колекціях

Уявіть, що ви використовуєте об’єкти як ключі у HashMap або елементи у HashSet. Якщо equals і hashCode реалізовані неправильно — колекції поводитимуться дивно: не знайдуть елемент, який ви щойно додали, або, навпаки, вважатимуть два різні об’єкти однаковими.

З record‑класами ви можете бути певні: порівняння й хеш‑код завжди враховують усі компоненти record (у тому порядку, в якому вони оголошені).

Приклад: використання record як ключа


import java.util.HashMap;
import java.util.Map;

public class Demo {
    public static void main(String[] args) {
        record Point(int x, int y) {}

        Map<Point, String> map = new HashMap<>();
        Point p1 = new Point(3, 4);
        map.put(p1, "Hello!");

        Point p2 = new Point(3, 4);
        System.out.println(map.get(p2)); // "Hello!" — працює!
    }
}

Зверніть увагу: p1 і p2 — різні об’єкти (різні посилання), але містять однакові значення полів, тому вважаються рівними. А докладніше про Map і HashMap ви дізнаєтеся на 26‑му рівні :P

Зручність налагодження та логування

Замість нудного Point@1a2b3c4d (як це буває за замовчуванням у звичайних класах), record‑клас друкується гарно та інформативно:

Point[x=3, y=4]

Це чудово заощаджує час під час налагодження та логування.

3. Як працюють equals, hashCode, toString всередині record

Метод equals

Record‑клас реалізує equals так, що два об’єкти вважаються рівними, якщо:

  • Вони одного типу (одного й того самого record‑класу)
  • Усі їхні компоненти рівні (== для примітивів, equals() для об’єктів)

Приклад порівняння


Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
Point p3 = new Point(1, 3);

System.out.println(p1.equals(p2)); // true
System.out.println(p1.equals(p3)); // false

Метод hashCode

Хеш‑код обчислюється за всіма компонентами record, зазвичай за допомогою стандартного методу Objects.hash(...).


System.out.println(p1.hashCode()); // Наприклад, 994
System.out.println(p2.hashCode()); // Теж 994
System.out.println(p3.hashCode()); // Інше число

Метод toString

Рядкове подання завжди має формат:

ClassName[field1=value1, field2=value2, ...]

System.out.println(p1); // Point[x=1, y=2]

4. Перевизначення equals, hashCode, toString: коли і як?

Іноді (рідко, але трапляється) потрібно змінити стандартну поведінку цих методів. Наприклад, ви хочете, щоб toString повертав рядок іншого формату, або щоб порівняння відбувалося лише за частиною полів.

Увага: якщо ви перевизначаєте equals/hashCode, робіть це дуже зважено! Порушення їхнього «контракту» може призвести до багів, які важко відстежувати.

Як перевизначити метод

Просто оголосіть свій метод усередині тіла record‑класу:


public record Point(int x, int y) {
    @Override
    public String toString() {
        return "(" + x + "; " + y + ")";
    }
}

Point p = new Point(3, 5);
System.out.println(p); // (3; 5)

Чи можна перевизначати equals/hashCode?

Так, але вкрай не рекомендується, якщо ви не впевнені, що робите. Наприклад, якщо ви хочете, щоб порівняння відбувалося лише за полем x (що саме по собі дивно):


public record Point(int x, int y) {
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point other)) return false;
        return x == other.x;
    }

    @Override
    public int hashCode() {
        return Integer.hashCode(x);
    }
}

Point p1 = new Point(1, 2);
Point p2 = new Point(1, 999);

System.out.println(p1.equals(p2)); // true (!)

Але будьте обережні: якщо ви перевизначаєте equals, завжди перевизначайте й hashCode — інакше колекції працюватимуть некоректно.

Найкращі практики

  • Якщо не знаєте точно, навіщо перевизначати — не перевизначайте!
  • Для toString можна сміливо робити свій формат, якщо потрібно.
  • Для equals/hashCode — тільки якщо є вагома причина, і ви розумієте наслідки.

5. Практика: порівняння об’єктів і використання record у колекціях

Приклад: порівняння двох record‑об’єктів


public record User(String name, int age) {}

public class Demo {
    public static void main(String[] args) {
        User u1 = new User("Alice", 20);
        User u2 = new User("Alice", 20);
        User u3 = new User("Bob", 25);

        System.out.println(u1.equals(u2)); // true
        System.out.println(u1.equals(u3)); // false

        System.out.println(u1.hashCode() == u2.hashCode()); // true
        System.out.println(u1); // User[name=Alice, age=20]
    }
}

Приклад: використання record як ключа в HashMap

Уявімо, що маємо застосунок, де зберігаємо кількість відвідувань користувачів за їхнім ім’ям і віком (скажімо, у клубі можуть бути двоє «Іван, 20 років»).


import java.util.HashMap;
import java.util.Map;

public class Demo {
    public static void main(String[] args) {
        record User(String name, int age) {}

        Map<User, Integer> visits = new HashMap<>();
        User ivan20 = new User("Ivan", 20);
        User ivan22 = new User("Ivan", 22);

        visits.put(ivan20, 5);
        visits.put(ivan22, 2);

        // Перевірмо, що пошук за значенням працює коректно
        System.out.println(visits.get(new User("Ivan", 20))); // 5
        System.out.println(visits.get(new User("Ivan", 22))); // 2
    }
}

Якби equals і hashCode не були реалізовані правильно, пошук не спрацював би. А докладніше про Map і HashMap ви дізнаєтеся з лекцій 26‑го рівня :P

6. Типові помилки під час роботи з equals, hashCode, toString у record‑класах

Помилка № 1: Очікування, що поля можна змінювати після створення.
Поля record завжди final, і порівняння відбувається за їхніми значеннями, заданими в конструкторі. Якщо ви якимось обхідним способом «змінюєте» внутрішній стан (наприклад, через змінюваний об’єкт усередині поля), порівняння й хеш‑код можуть стати некоректними.

Помилка № 2: Перевизначили equals, але забули про hashCode.
Якщо ви перевизначаєте один із цих методів — завжди перевизначайте і другий! Інакше колекції (HashSet, HashMap) поводитимуться непередбачувано.

Помилка № 3: Очікування, що toString буде в іншому форматі.
Якщо вам потрібен особливий формат рядка — просто перевизначте toString. За замовчуванням формат завжди ClassName[field1=value1, field2=value2].

Помилка № 4: Використання record для складних класів із змінними полями.
Поля record мають бути незмінними. Якщо як поле ви використовуєте, наприклад, ArrayList, і хтось змінює його вміст — порівняння й хеш‑код можуть «зламатися». Для record краще використовувати лише незмінні типи.

Помилка № 5: Використання record для класів із поведінкою, що не відповідає моделі value‑об’єкта.
Record — це не «маленький клас із коротким синтаксисом». Це саме value‑об’єкт, призначений для зберігання набору значень. Якщо у вас складна логіка, змінний стан або потрібне наслідування — використовуйте звичайний клас.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ