Вопрос 2 · Раздел 20

В чём основные отличия Record от обычного класса

// java.lang.Record — специальный класс, который нельзя использовать напрямую через extends.

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Record — это специальный тип класса в Java, предназначенный для хранения данных. Главное отличие: Record создан быть простым контейнером данных, а обычный класс — универсальным инструментом.

Основные отличия:

| Record | Обычный класс | | ———————————– | ———————– | | Все поля private final | Поля любые | | Нельзя изменить поля после создания | Можно менять поля | | Наследуется от java.lang.Record | Наследуется от Object | // java.lang.Record — специальный класс, который нельзя использовать напрямую через extends. | implicit final — нельзя расширять | Можно наследовать | | Автогенерация equals, hashCode, toString | Нужно писать вручную или через IDE |

Пример:

// Record — одна строка
public record Point(int x, int y) {}

// Обычный класс — 30+ строк
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; }
    
    // ещё equals, hashCode, toString...
}

🟡 Middle Level

Как это работает

Record — это ограниченная форма класса, где компилятор берёт на себя boilerplate:

1. Конструктор:

// Record — канонический конструктор автогенерируется
public record User(String name, int age) {}

// Можно добавить свой конструктор (должен вызывать канонический)
public record User(String name, int age) {
    public User(String name) {
        this(name, 0);  // делегирование
    }
}

2. Геттеры:

// Record — без приставки get!
User user = new User("John", 25);
user.name();  // ✅ NOT user.getName()
user.age();   // ✅ NOT user.getAge()

3. Наследование:

// ❌ Record нельзя расширить
public class Bad extends Point {}  // compilation error

// ❌ Record не может ничего расширять (кроме java.lang.Record)
public record Bad extends Object {}  // compilation error

// ✅ Record может имплементировать интерфейсы
public record User(String name) implements Serializable {}

Практическое применение

Когда использовать Record:

  • DTO для REST API
  • Ключи в HashMap/HashSet
  • Возврат нескольких значений из метода
  • Value objects в DDD

Когда обычный класс:

  • Нужно mutable состояние
  • Нужны дополнительные методы/поля
  • Нужно наследование
  • Нужен builder pattern с mutable полями

Типичные ошибки

  1. Попытка сделать mutable поля: ```java public record BadRecord() { // ❌ Не static поле — ошибка компиляции public int mutableField = 0; }

// ✅ Только static public record GoodRecord() { public static int counter = 0; }


2. **Ожидание get/set методов:**
```java
public record User(String name) {}
User u = new User("John");

// ❌ u.getName() — такого нет!
// ✅ u.name() — правильный вызов

🔴 Senior Level

Internal Implementation

Class file structure:

// Record — это final class с ACC_RECORD флагом
ClassFile {
    access_flags: ACC_FINAL | ACC_RECORD
    super_class: java/lang/Record
    fields: все private final
    methods: канонический конструктор + аксессоры + equals/hashCode/toString
}

Отличия на уровне JVM:

  • Record имеет специальный атрибут RecordComponents в constant pool
  • JVM знает о канонических компонентах (имя, тип, аннотации)
  • Reflection API: Class.getRecordComponents() возвращает RecordComponent[]

Архитектурные Trade-offs

Record:

  • ✅ Минимум boilerplate
  • ✅ Гарантированная иммутабельность
  • ✅ Оптимизация под value types (Project Valhalla)
  • ❌ Нет гибкости
  • ❌ Нельзя использовать с JPA entities (нужен no-arg конструктор)

Обычный класс:

  • ✅ Полный контроль
  • ✅ JPA совместимость
  • ✅ Builder, mutable state
  • ❌ Больше кода
  • ❌ Ручная поддержка equals/hashCode

Edge Cases

1. Компактный конструктор для валидации:

public record Email(String value) {
    public Email {
        if (!value.contains("@")) {
            throw new IllegalArgumentException("Invalid email");
        }
    }
}

2. Аннотации на компонентах:

public record User(
    @JsonProperty("user_name") String name,
    @Min(18) int age
) {}
// Аннотация применяется к полю, параметру конструктора И аксессору

3. Сериализация:

// Record использует специальный механизм десериализации
// через канонический конструктор (JEP 445, Java 21)
// вместо readObject/writeObject

Производительность

Операция           | Record | Обычный класс (final поля)
-------------------|--------|---------------------------
Создание объекта    | 8 ns   | 8 ns
equals()           | 15 ns  | 15 ns (ручная реализация)
hashCode()         | 12 ns  | 12 ns

Разница минимальна — Record не уступает
// Примерные значения. Зависят от JVM/hardware.

Production Experience

// DTO в Spring Boot 3.x — Records стали стандартом
public record CreateUserRequest(
    @NotBlank String name,
    @Email String email,
    @Min(18) int age
) {}

// JPA entities — пока нельзя использовать Records
// (нужен mutable state, no-arg constructor, lazy loading proxies)
@Entity
public class User {  // обычный класс
    @Id private Long id;
    private String name;
}

Best Practices

// ✅ Record для immutable DTO
public record OrderDto(String id, Instant createdAt, BigDecimal amount) {}

// ✅ Record для value objects
public record Money(BigDecimal amount, Currency currency) {
    public Money {
        Objects.requireNonNull(amount);
        Objects.requireNonNull(currency);
    }
}

// ❌ Record для JPA entity
// ❌ Record для mutable коллекций без защиты
public record BadRecord(int[] values) {}  // массив mutable!

🎯 Шпаргалка для интервью

Обязательно знать:

  • Record — ограниченный вид класса: только final поля, нельзя наследовать, нельзя наследовать самому
  • Record наследуется от java.lang.Record, обычный класс — от Object
  • Автогенерация equals, hashCode, toString — Record vs ручной код в обычном классе
  • Record immutable по дизайну; обычный класс может быть mutable или immutable
  • Record нельзя использовать с JPA (нужен no-arg конструктор, mutable state)
  • Record геттеры без get: user.name() вместо user.getName()

Частые уточняющие вопросы:

  • Когда выбирать Record, а когда обычный класс? — Record для immutable DTO/value objects, обычный класс для JPA, mutable state, builder
  • Можно ли добавить дополнительные методы в Record? — Да, любые static и instance методы, но не instance поля
  • Record быстрее обычного класса? — Нет, производительность идентична, разница < 5%
  • Можно ли переопределить equals/hashCode в Record? — Да, но это редко нужно

Красные флаги (НЕ говорить):

  • ❌ “Record — это подкласс обычного класса” — Record наследуется от java.lang.Record, специального класса
  • ❌ “Record поддерживает наследование” — Record implicit final
  • ❌ “Record использует get/set” — аксессоры без get: name() не getName()
  • ❌ “Record подходит для JPA” — JPA требует no-arg конструктор и mutable поля

Связанные темы:

  • [[1. Что такое Record в Java и с какой версии они доступны]]
  • [[3. Можно ли наследоваться от Record или наследовать Record от другого класса]]
  • [[5. Какие методы автоматически генерируются для Record]]
  • [[9. Являются ли поля Record финальными]]