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‑об’єкт, призначений для зберігання набору значень. Якщо у вас складна логіка, змінний стан або потрібне наслідування — використовуйте звичайний клас.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ