Які методи автоматично генеруються для Record
Коли ви оголошуєте record, компілятор Java автоматично створює 5 типів методів:
🟢 Junior Level
Коли ви оголошуєте record, компілятор Java автоматично створює 5 типів методів:
- Канонічний конструктор: приймає всі компоненти в порядку їхнього оголошення в заголовку та присвоює їх відповідним
private finalполям. - Публічні методи-аксесори (геттери): генеруються для кожного компонента з ім’ям, що точно збігається з ім’ям компонента (наприклад,
user.name(),user.age()— строго без префіксаget!). equals(Object o): повертаєtrue, якщо об’єкт для порівняння належить до того самого типу запису і всі його компоненти є еквівалентними.hashCode(): обчислює хеш-код на основі значень усіх компонентів запису.toString(): повертає зрозуміле рядкове представлення виглядуRecordName[field1=val1, field2=val2].
public record User(String name, int age) {}
// Використання всіх автогенерованих методів:
User user1 = new User("Alice", 25); // 1. Канонічний конструктор
User user2 = new User("Alice", 25);
System.out.println(user1.name()); // 2. Аксесор: "Alice"
System.out.println(user1.age()); // 2. Аксесор: 25
System.out.println(user1.equals(user2)); // 3. true (порівняння за полями)
System.out.println(user1.hashCode() == user2.hashCode()); // 4. true (ідентичні хеші)
System.out.println(user1); // 5. "User[name=Alice, age=25]"
🟡 Middle Level
1. Як працюють згенеровані методи порівняння
equals(Object o):- Спочатку перевіряє посилальну рівність (
this == o). - Перевіряє точний тип:
o != null && this.getClass() == o.getClass(). - Порівнює примітивні поля через оператор
==(дляfloat/doubleвикликаютьсяFloat.compareтаDouble.compareдля уникнення аномалій ізNaNта-0.0). - Порівнює посилальні поля через
Objects.equals(this.field, other.field).
- Спочатку перевіряє посилальну рівність (
hashCode():- Комбінує хеш-коди всіх компонентів за алгоритмом множення на непарне просте число ($31$), гарантуючи коректний контракт з
equals()для роботи вHashMapтаHashSet.
- Комбінує хеш-коди всіх компонентів за алгоритмом множення на непарне просте число ($31$), гарантуючи коректний контракт з
2. Критична пастка: Record з масивами (byte[], int[])
[!CAUTION] Автогенеровані методи
equals(),hashCode()таtoString()не вміють працювати з масивами за їхнім вмістом! Для масивів методObjects.equals(a, b)порівнює посилання на область пам’яті, а не самі елементи!
public record BinaryPayload(String id, byte[] data) {}
BinaryPayload p1 = new BinaryPayload("1", new byte[]{1, 2, 3});
BinaryPayload p2 = new BinaryPayload("1", new byte[]{1, 2, 3});
// ❌ Проблема: різні масиви в купі, посилання не рівні!
System.out.println(p1.equals(p2)); // FALSE!
System.out.println(p1); // BinaryPayload[id=1, data=[B@6bdf28bb] (адреса масиву!)
Рішення: Явне перевизначення через Arrays
Якщо у вашому Record присутній масив, ви зобов’язані вручну перевизначити всі три методи:
public record BinaryPayload(String id, byte[] data) {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BinaryPayload that)) return false;
return Objects.equals(id, that.id) && Arrays.equals(data, that.data);
}
@Override
public int hashCode() {
int result = Objects.hashCode(id);
return 31 * result + Arrays.hashCode(data);
}
@Override
public String toString() {
return "BinaryPayload[id=" + id + ", data=" + Arrays.toString(data) + "]";
}
}
🔴 Senior Level
Внутрішній устрій у байткоді: invokedynamic та ObjectMethods.bootstrap
Всупереч поширеній помилковій думці, компілятор javac не генерує довгі простирадла байткоду для кожного методу equals, hashCode та toString усередині кожного файлу .class.
Замість цього компілятор генерує виклик інструкції invokedynamic (indy), що вказує на системний бутстрап-метод:
// Декомпіляція через javap -v:
public final boolean equals(java.lang.Object);
descriptor: (Ljava/lang/Object;)Z
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
0: aload_0
1: aload_1
2: invokedynamic #17, 0 // BootstrapMethods #0:equals:(LUser;Ljava/lang/Object;)Z
7: ireturn
Бутстрап-метод: java.lang.runtime.ObjectMethods.bootstrap(...)
Під час першого виконання JVM викликає:
public static Object bootstrap(
MethodHandles.Lookup lookup,
String methodName,
TypeDescriptor type,
Class<?> recordClass,
String names,
MethodHandle... getters
)
- JVM передає список
MethodHandleдля кожного компонента запису. - Метод
ObjectMethodsна льоту компонує оптимізований ланцюжок викликівMethodHandleпід конкретний тип запису. - JIT-компілятор (C2) бездоганно оптимізує та інлайнить сгенеровані дескриптори безпосередньо в машинний код процесора.
Переваги архітектури на invokedynamic:
- Мінімальний розмір
.classфайлів (відсутні сотні байтів однотипних інструкцій). - Можливість оптимізації алгоритмів хешування та порівняння в майбутніх версіях JDK без необхідності перекомпіляції користувацького коду.
Чому Record не кешує свій hashCode?
Клас java.lang.String кешує свій хеш-код у приватному мутабельному полі private int hash. При першому виклику hashCode() він обчислюється, а під час наступних повертається миттєво за $O(1)$.
У Record такий прийом неможливий:
- У тілі
recordзаборонено оголошувати будь-які нестатичні поля екземпляра (instance fields). - Створити приховане мутабельне поле
private int cachedHashCode = 0;компілятор не дозволить (JLS §8.10). - Кожного разу при виклику
record.hashCode()обчислення виконується заново за всіма полями. Якщо в записі містяться важкі вкладені структури (наприклад, рядки на мільйон символів або глибокі колекції), частий викликhashCode()у циклі може стати вузьким місцем (CPU bottleneck).
4 Tricky Questions
1. Як компілятор реалізує equals, hashCode та toString у байткоді: генерує класичний код чи використовує invokedynamic?
Відповідь:
Компілятор використовує інструкцію invokedynamic із бутстрап-методом java.lang.runtime.ObjectMethods.bootstrap.
У байткоді класу запису немає жорстко закодованих ланцюжків if-else чи обчислень хешу. Під час першого завантаження бутстрап-метод динамічно компонує ланцюжок дескрипторів MethodHandle для читання полів конкретного запису та пов’язує їх із CallSite. Це кардинально зменшує розмір байткоду і дозволяє віртуальній машині на льоту застосовувати глибокі оптимізації JIT-компілятора.
2. Що станеться з автогенерованими equals(), hashCode() та toString(), якщо одним із компонентів Record є масив (наприклад, byte[] payload)?
Відповідь: Виникне логічна помилка порівняння:
- Автогенерований
equals()використовуєObjects.equals(), який для масивів порівнює їхні посилання в пам’яті (==), а не вміст. Два різних об’єкти Record з ідентичними даними в масивах повернутьfalseпри виклику.equals(). hashCode()поверне identity-хешкод масиву (за його адресою в пам’яті), а не хеш вмісту елементів.toString()надрукує тип і системний хеш масиву (наприклад,[B@4f023edb), а не зрозумілий масив. Висновок: Якщо компонент є масивом, розробник зобов’язаний вручну перевизначити всі три методи з використанням утилітного класуjava.util.Arrays.
3. Чи можна реалізувати ліниве кешування hashCode всередині Record, як це зроблено в класі String?
Відповідь:
Напряму в полях об’єкта — не можна.
Специфікація мови Java (JLS §8.10) категорично забороняє оголошення будь-яких нестатичних полів усередині тіла record, окрім канонічних компонентів, оголошених у заголовку. Створити змінну private int hash; неможливо. Хеш-код обчислюється заново при кожному зверненні.
Якщо кешування критичне для швидкодії, необхідно або використовувати зовнішній кеш (наприклад, ConcurrentHashMap), або перейти від Record до класичного final class із ручним кешуванням хешу.
4. Чи генерує Record метод clone() або спеціальні методи для Java Serialization (readObject / writeObject)?
Відповідь: Ні, не генерує.
- Метод
clone()не генерується і не підтримується. ОскількиRecordє незмінним (immutable), клонування не має сенсу (копіювання посилання є достатнім, а для створення змінених копій застосовується патерн Wither). - Методи
readObjectтаwriteObjectне генеруються, а якщо їх оголосити вручну, JVM їх проігнорує. Механізм серіалізації для записів жорстко зафіксований на рівні платформи: десеріалізація виконується виключно через виклик канонічного конструктора.
🎯 Шпаргалка для інтерв’ю
5 автоматично генерованих методів Record
| Метод | Сигнатура | Особливості реалізації |
|---|---|---|
| Канонічний конструктор | User(String name, int age) |
Ініціалізує private final поля в порядку оголошення. |
| Аксесори | name(), age() |
Імена строго без префікса get. Повертають значення полів. |
equals(Object o) |
boolean equals(Object o) |
Реалізований через invokedynamic (ObjectMethods.bootstrap). |
hashCode() |
int hashCode() |
Хеш комбінації всіх компонентів ($31 \times \dots$). Не кешується! |
toString() |
String toString() |
Повертає: RecordName[comp1=val1, comp2=val2]. |
Пам’ятка розробника
- Масиви в Record: Автогенерація порівнює посилання. Завжди перевизначайте
equals,hashCodeтаtoStringчерезArrays.*. - Байткод під капотом:
invokedynamic+ObjectMethods.bootstrap— індикатор глибокого розуміння JVM на співбесіді. - Кешування: Полів для кешу додати не можна, розрахунок хешу відбувається при кожному виклику.