Що таке 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]]