Что такое Record в Java и с какой версии они доступны
Record позволяет в одну строку объявить класс, который автоматически получает:
🟢 Junior Level
Record — это специальный тип класса в Java, предназначенный для хранения неизменяемых данных. Он появился как превью-фича в Java 14 (JEP 359) и стал полноценной стандартной фичей в Java 16 (JEP 395).
Record позволяет в одну строку объявить класс, который автоматически получает:
- приватные
finalполя - публичный конструктор
- геттеры (без приставки
get) equals(),hashCode()иtoString()
Пример:
// Вместо 50+ строк кода — одна строка
public record User(String name, int age, String email) {}
// Использование
User user = new User("John", 25, "john@example.com");
System.out.println(user.name()); // John
System.out.println(user.age()); // 25
Когда использовать:
- DTO (Data Transfer Objects)
- Классы для хранения результатов запросов из БД
- Ключи в коллекциях (HashMap, HashSet)
- Возврат нескольких значений из метода
- Любые ситуации, когда нужен простой immutable-класс
🟡 Middle Level
Как это работает
Record — это не просто “сахар”, это особый вид класса с жёсткими ограничениями:
// То, что вы пишете:
public record Point(int x, int y) {}
// То, что генерирует компилятор:
public final class Point extends java.lang.Record {
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) { /* автогенерированный */ }
@Override
public int hashCode() { /* автогенерированный */ }
@Override
public String toString() { /* автогенерированный */ }
}
Практическое применение
1. DTO в Spring Boot:
public record CreateUserRequest(
@NotBlank String name,
@Email String email,
@Min(18) int age
) {}
@RestController
public class UserController {
@PostMapping("/users")
public ResponseEntity<UserResponse> createUser(
@Valid @RequestBody CreateUserRequest request
) {
// request.name(), request.email() — доступ к полям
}
}
2. Возврат нескольких значений:
public record CalculationResult(int result, int operationTimeMs) {}
public CalculationResult calculate() {
// ... сложная логика
return new CalculationResult(42, 150);
}
3. Pattern matching в switch (Java 21+):
public sealed interface Shape permits Circle, Rectangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double width, double height) implements Shape {}
public double getArea(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
};
}
Типичные ошибки
- Ошибка: попытка сделать Record mutable
// ❌ НЕЛЬЗЯ — поля всегда final public record BadRecord() { public int mutableField = 0; // compilation error, если не static } - Ошибка: мутабельные компоненты в Record
// ⚠️ Осторожно — массив mutable! public record DataWithArray(int[] values) {} DataWithArray d1 = new DataWithArray(new int[]{1, 2, 3}); d1.values()[0] = 99; // можно изменить! // ✅ Лучше — использовать List.of() или делать копию public record SafeData(List<Integer> values) {} - Ошибка: null в Record
// Record допускает null по умолчанию public record User(String name) {} User user = new User(null); // OK, но может привести к NPE // ✅ Решение — null check в конструкторе (Java 16+) public record User(String name) { public User { Objects.requireNonNull(name, "Name cannot be null"); } }
Сравнение с альтернативами
| Подход | Плюсы | Минусы |
|---|---|---|
| Record | Минимум кода, immutable, автогенерация | Нельзя наследовать, только final поля |
| Lombok @Data | Больше гибкости, mutable/immutable | Зависимость от Lombok, проблемы с IDE |
| Ручной класс | Полный контроль | Много boilerplate, ошибки в equals/hashCode |
| Класс с @Value | Immutable, понятный | Больше кода, чем Record |
🔴 Senior Level
Internal Implementation
Record — это не просто syntactic sugar, это новая модель данных в JVM.
Ключевые JEP:
- JEP 359: Records (Preview, Java 14)
- JEP 384: Records (Second Preview, Java 15)
- JEP 395: Records (Final, Java 16)
Внутреннее устройство:
// Компилятор добавляет скрытые атрибуты:
// - ACC_RECORD flag в class file
// - RecordComponents attribute в constant pool
// Runtime можно получить компоненты через reflection:
RecordComponent[] components = Point.class.getRecordComponents();
for (RecordComponent rc : components) {
System.out.println(rc.getName()); // "x", "y"
System.out.println(rc.getType()); // int, int
System.out.println(rc.getAccessor()); // Method reference
}
Class file structure:
ClassFile {
u2 access_flags; // ACC_RECORD | ACC_FINAL
u2 this_class;
u2 super_class; // java/lang/Record
...
Record_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 components_count;
record_component_info components[];
}
}
Архитектурные Trade-offs
Record vs обычный класс:
| Аспект | Record | Обычный класс |
|---|---|---|
| Наследование | Нельзя (implicit final) | Можно |
| Поля | Только final | Любые |
| Интерфейсы | Можно имплементировать | Можно |
| sealed | Можно | Можно |
| Аннотации | На поля, конструктор | На что угодно |
| Serialization | Своя логика (JEP 445) | Стандартная |
Почему нельзя расширять Record:
java.lang.Record — специальный суперкласс, недоступный для прямого extends.
Это сделано для:
1. Гарантии иммутабельности
2. Предсказуемого equals/hashCode
3. Оптимизации JVM (value types совместимость)
4. Pattern matching работы
Edge Cases
1. Generic Records:
public record ApiResponse<T>(int status, T data, String message) {}
// Использование
ApiResponse<List<User>> response = new ApiResponse<>(200, users, "OK");
ApiResponse<Optional<User>> maybeUser = new ApiResponse<>(404, Optional.empty(), "Not found");
2. Recursive Records (для linked структур):
public record ListNode<T>(T value, ListNode<T> next) {}
ListNode<String> list = new ListNode<>("A",
new ListNode<>("B",
new ListNode<>("C", null)));
3. Records с аннотациями:
public record User(
@JsonProperty("user_name") @NonNull String name,
@Min(0) @Max(150) int age
) {
// Аннотация на уровне компонента применяется к:
// - полю
// - параметру конструктора
// - методу-аксессору
}
// Для точного контроля — аннотации на целевые элементы:
public record Config(
String key
) {}
// В Java аннотации на компонентах Record применяются к полю, параметру и аксессору.
// Для целевого применения используйте meta-аннотации @Target({FIELD, METHOD, PARAMETER}).
4. Records и Serialization (Java 21+):
// Record serialization: при десериализации вызывается канонический конструктор,
// а не readObject. Это стандартная Java serialization (не JEP 445).
record SerializableData(String name, int value) implements Serializable {}
// JVM автоматически валидирует:
// 1. Все финальные поля инициализированы
// 2. Инварианты сохранены
// 3. Можно использовать компактный конструктор для валидации
Производительность
Память:
Record vs Class с тем же функционалом:
- Заголовок объекта: 12-16 байт (зависит от JVM)
- Поля: как обычно (int = 4, long = 8, reference = 4-8)
- Lombok генерирует код на этапе компиляции — runtime overhead отсутствует в обоих случаях.
- Нет дополнительных полей для equals/hashCode кеша
Время выполнения:
equals() / hashCode() автогенерированные:
- Компилятор создаёт оптимальную реализацию
- Использует Objects.hash() или Arrays.hashCode()
- Performance сравним с ручной реализацией
- JIT может инлайнить лучше (final поля)
Бенчмарк (JMH, Java 21):
// Примерные значения (JMH, Java 21). Зависят от JVM и hardware.
Операция | Record | Lombok @Value | Ручной класс
-------------------|--------|---------------|--------------
equals() | 15 ns | 15 ns | 14 ns
hashCode() | 12 ns | 13 ns | 12 ns
toString() | 45 ns | 48 ns | 46 ns
Создание объекта | 8 ns | 8 ns | 8 ns
Разница < 5% — Record не уступает по производительности
Production Experience
Реальный кейс — миграция DTO на Records:
// До (Spring Boot 2.x, Lombok):
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
private String name;
private String email;
private int age;
}
// После (Spring Boot 3.x, Java 17+):
public record UserDto(String name, String email, int age) {}
// Результат:
// - Код сократился с ~15 строк до 1
// - Убрали зависимость Lombok для этих классов
// - DTO стали immutable (защита от случайных изменений)
// - Jackson сериализует автоматически (через конструктор)
Проблема с Jackson и Records:
// Jackson 2.12+ поддерживает Records нативно
// Но могут быть нюансы:
public record Order(@JsonProperty("order_id") String id) {}
// ✅ Работает из коробки в Spring Boot 2.4+
// ⚠️ В старых версиях нужно:
// 1. Обновить Jackson до 2.12+
// 2. Или добавить @JsonCreator на канонический конструктор
Monitoring
Отладка Records:
# Декомпиляция .class файла
javap -p UserRecord.class
# Вывод покажет:
# final class UserRecord extends java.lang.Record
# private final java.lang.String name
# private final int age
# public UserRecord(java.lang.String, int)
# public java.lang.String name()
# public int age()
Рефлексия для инспекции:
public static void inspectRecord(Object record) {
Class<?> clazz = record.getClass();
if (!clazz.isRecord()) {
throw new IllegalArgumentException("Not a record");
}
RecordComponent[] components = clazz.getRecordComponents();
System.out.println("Record: " + clazz.getSimpleName());
for (RecordComponent rc : components) {
Method accessor = rc.getAccessor();
Object value = accessor.invoke(record);
System.out.println(" " + rc.getName() + " = " + value);
}
}
Future Trends
Java 21+:
// Record Patterns (JEP 440) — Pattern matching для Record
Point p = new Point(42, 0);
if (p instanceof Point(int x, int y)) {
System.out.println(x + y); // 42
}
// Switch с Record Patterns
double calcArea(Shape s) {
return switch (s) {
case Circle(double r) -> Math.PI * r * r;
case Rectangle(double w, double h) -> w * h;
case Triangle(double b, double h) -> 0.5 * b * h;
};
}
// Sealed + Records + Pattern Matching = мощный набор
public sealed interface Expr
permits Num, Add, Mul {}
public record Num(int value) implements Expr {}
public record Add(Expr left, Expr right) implements Expr {}
public record Mul(Expr left, Expr right) implements Expr {}
int eval(Expr e) {
return switch (e) {
case Num(int n) -> n;
case Add(Expr l, Expr r) -> eval(l) + eval(r);
case Mul(Expr l, Expr r) -> eval(l) * eval(r);
};
}
Best Practices для Highload
// 1. Используйте Records для immutable DTO
public record OrderDto(String id, Instant createdAt, BigDecimal amount) {}
// 2. Валидация в компактном конструкторе
public record UserId(String value) {
public UserId {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Invalid ID");
}
if (value.length() > 36) {
throw new IllegalArgumentException("ID too long");
}
}
}
// 3. Records как ключи в кеше — идеально (immutable + hashCode)
public record CacheKey(String tenantId, String entityType, String entityId) {}
Map<CacheKey, Object> cache = new ConcurrentHashMap<>();
// 4. Используйте для event sourcing
public record DomainEvent(
UUID eventId,
String aggregateId,
String eventType,
Instant occurredAt,
Map<String, Object> payload
) {}
🎯 Шпаргалка для интервью
Обязательно знать:
- Records появились как preview в Java 14 (JEP 359), стали стабильными в Java 16 (JEP 395)
- Record — это специальный тип класса, наследующий
java.lang.Record - Автоматически генерирует: канонический конструктор, аксессоры,
equals(),hashCode(),toString() - Аксессоры называются по имени поля:
user.name(), а НЕuser.getName() - Record идеально подходит для DTO, ключей в HashMap, value objects
- Record не поддерживает наследование — implicit final
- Начиная с Java 21 доступен pattern matching для Records (JEP 440)
Частые уточняющие вопросы:
- С какой версии Records стали стабильными? — Java 16 (JEP 395), preview — Java 14
- Можно ли сделать Record mutable? — Нет, все поля автоматически final
- Как Record работает с Jackson? — Jackson 2.12+ поддерживает нативно через канонический конструктор
- Чем Record отличается от Lombok @Value? — Record — стандарт языка, меньше кода, нет зависимости от Lombok
Красные флаги (НЕ говорить):
- ❌ “Record — это просто syntactic sugar для Lombok” — Record — полноценная модель данных в JVM
- ❌ “Можно наследовать Record” — Record implicit final, наследование запрещено
- ❌ “Record имеет get/set методы” — аксессоры без приставки get:
name()вместоgetName() - ❌ “Record медленнее обычного класса” — производительность идентична (разница < 5%)
Связанные темы:
- [[2. В чём основные отличия Record от обычного класса]]
- [[5. Какие методы автоматически генерируются для Record]]
- [[9. Являются ли поля Record финальными]]
- [[10. Можно ли использовать Record как ключ в HashMap]]