Вопрос 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]]