Питання 5 · Розділ 20

Які методи автоматично генеруються для Record

Коли ви створюєте Record, компілятор автоматично генерує 5 типів методів:

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Коли ви створюєте Record, компілятор автоматично генерує 5 типів методів:

  1. Конструктор — приймає всі поля в порядку оголошення
  2. Геттери — для кожного поля (без префікса get)
  3. equals() — порівнює всі поля
  4. hashCode() — хеш всіх полів
  5. toString() — рядкове представлення
public record User(String name, int age) {}

// Автогенеровані методи:
User user = new User("John", 25);  // конструктор

user.name();   // "John" — геттер для name
user.age();    // 25 — геттер для age

user.equals(other);  // порівнює name та age
user.hashCode();     // хеш від name та age
user.toString();     // "User[name=John, age=25]"

🟡 Middle Level

Як це працює

1. Канонічний конструктор:

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

// Автогенерований конструктор:
public User(String name, int age) {
    this.name = name;
    this.age = age;
}

2. Аксесори (геттери):

// Ім'я метода = ім'я поля (НЕ getName()!)
public String name() { return name; }
public int age() { return age; }

3. equals() — порівнює всі компоненти:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof User other)) return false;
    return Objects.equals(name, other.name) && age == other.age;
}

4. hashCode() — від всіх полів:

@Override
public int hashCode() {
    // Насправді компілятор генерує оптимізований hashCode:
    // 31 * name.hashCode() + age (пряма арифметика, не Objects.hash).
    // Objects.hash() створює масив і повільніше — це концептуальне спрощення.
    return Objects.hash(name, age);
}

5. toString() — читабельне представлення:

@Override
public String toString() {
    return "User[name=" + name + ", age=" + age + "]";
}

Типові помилки

  1. Очікування get/set: ```java User user = new User(“John”, 25);

// ❌ user.getName() — такого методу немає! // ❌ user.setName(“Jane”) — Record immutable! // ✅ user.name() — правильний виклик


2. **Спроба перевизначити аксесор:**
```java
public record Point(int x, int y) {
    // ❌ Не можна змінити тип або сигнатуру аксесора
    public String x() { return String.valueOf(x); }  // error
}

Практичне застосування

Record як ключ в HashMap:

public record CacheKey(String tenantId, String entityType, String entityId) {}

Map<CacheKey, Object> cache = new ConcurrentHashMap<>();
cache.put(new CacheKey("t1", "user", "u1"), userData);

// equals і hashCode працюють коректно
CacheKey key = new CacheKey("t1", "user", "u1");
cache.get(key);  // знайде userData

🔴 Senior Level

Internal Implementation

Class file атрибути:

// Компілятор додає:
- ACC_FINAL прапорець для класу
- ACC_RECORD прапорець
- RecordComponents attribute в constant pool
- Canonical constructor
- Accessor methods для кожного компонента
- equals, hashCode, toString з java.lang.Record (або перевизначені)

java.lang.Record базова реалізація:

// java.lang.Record надає дефолтні реалізації:
public abstract class Record {
    protected Record() {}

    @Override
    public abstract boolean equals(Object obj);
    @Override
    public abstract int hashCode();
    @Override
    public abstract String toString();
}

Автогенерація equals (псевдокод):

public boolean equals(Object other) {
    if (this == other) return true;
    if (other == null || getClass() != other.getClass()) return false;

    Record that = (Record) other;
    for (RecordComponent rc : this.getClass().getRecordComponents()) {
        Object thisValue = rc.getAccessor().invoke(this);
        Object thatValue = rc.getAccessor().invoke(that);
        if (!Objects.equals(thisValue, thatValue)) {
            return false;
        }
    }
    return true;
}

Архітектурні Trade-offs

Автогенерація vs ручна реалізація:

Аспект Автогенерація Ручна реалізація
Код 0 рядків 20-50 рядків
Помилки Немає Можливі
Кастомізація Немає Повна
Продуктивність Оптимальна Залежить від реалізації

Edge Cases

1. Перевизначення equals/hashCode:

public record CaseInsensitiveName(String name) {
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof CaseInsensitiveName other)) return false;
        return name.equalsIgnoreCase(other.name);
    }

    @Override
    public int hashCode() {
        return name.toLowerCase().hashCode();
    }
}

2. Lazy hashCode кешування:

// Instance поля (навіть volatile) не можна додавати в Record — це compilation error.

3. Кастомний toString:

public record SecretRecord(String publicData, String secretData) {
    @Override
    public String toString() {
        return "SecretRecord[public=" + publicData + ", secret=***]";
    }
}

Продуктивність

Операція           | Автогенерація | Ручна реалізація
-------------------|---------------|------------------
equals()           | 15 ns         | 14 ns
hashCode()         | 12 ns         | 10 ns
toString()         | 45 ns         | 40 ns

Різниця < 10% — автогенерація практично оптимальна

Production Experience

JPA проблема:

// ❌ Record не працює з Hibernate
// Hibernate вимагає:
// 1. No-arg constructor
// 2. Mutable поля для lazy loading
// 3. Proxy mechanism

@Entity
public record User(Long id, String name) {}  // НЕ працює!

// ✅ Звичайний клас
@Entity
public class User {
    @Id private Long id;
    private String name;
}

Best Practices

// ✅ Використовуйте автогенерацію для простих Record
public record Point(int x, int y) {}  // auto equals/hashCode/toString

// ✅ Перевизначайте тільки за необхідності
public record Password(String hash, String salt) {
    @Override
    public String toString() {
        return "Password[hash=***, salt=***]";  // приховуємо sensitive data
    }
}

// ❌ Не перевизначайте без причини
// ❌ Не змінюйте контракт equals/hashCode

🎯 Шпаргалка для співбесіди

Обов’язково знати:

  • Record автогенерує 5 типів: канонічний конструктор, аксесори, equals(), hashCode(), toString()
  • Аксесори без get: name() замість getName()
  • equals() порівнює всі компоненти через Objects.equals()
  • hashCode() обчислюється від всіх компонентів (оптимізований ланцюжок, не Objects.hash)
  • toString() формат: RecordName[field1=value1, field2=value2]
  • Можна перевизначити будь-який з методів за необхідності

Часті уточнюючі запитання:

  • Чи можна вимкнути автогенерацію equals? — Ні, але можна перевизначити своєю реалізацією
  • Як hashCode обчислюється? — Оптимізований ланцюжок множень (31 * h1 + h2…), не Objects.hash
  • Що якщо перевизначити equals але не hashCode? — Порушення контракту HashMap — баги!
  • Чи можна перевизначити тільки toString? — Так, часто роблять для приховування sensitive data

Червоні прапорці (НЕ говорити):

  • ❌ “equals() використовує ==” — equals() порівнює значення всіх полів через Objects.equals
  • ❌ “hashCode() кешується” — Ні, обчислюється щоразу (не можна додати field для кешу)
  • ❌ “Record генерує геттери з get” — Аксесори без get: name() не getName()
  • ❌ “toString() приховує дані” — toString() показує всі поля, потрібно перевизначити для sensitive data

Пов’язані теми:

  • [[1. Що таке Record в Java і з якої версії вони доступні]]
  • [[2. У чому основні відмінності Record від звичайного класу]]
  • [[6. Чи можна перевизначити конструктор в Record]]
  • [[10. Чи можна використовувати Record як ключ в HashMap]]