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

Чи можна наслідуватися від Record або наслідувати Record від іншого класу

Record створений для простої передачі даних, а не для об'єктно-орієнтованого наслідування.

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

🟢 Junior Level

Ні, не можна. Record в Java має два жорсткі обмеження:

  1. Record не можна розширити — Record є implicit final
  2. Record не може наслідувати інший клас — всі Record автоматично наслідуються від java.lang.Record
// ❌ Не можна наслідуватися від Record
public record Point(int x, int y) {}
public class Bad extends Point {}  // compilation error

// ❌ Record не може розширювати інший клас
public record Bad extends Object {}  // compilation error
// Record завжди extends java.lang.Record

Але можна імплементувати інтерфейси:

public record User(String name) implements Serializable, Comparable<User> {
    @Override
    public int compareTo(User other) {
        return this.name.compareTo(other.name);
    }
}

🟡 Middle Level

Чому такі обмеження?

Record створений для простої передачі даних, а не для об’єктно-орієнтованого наслідування.

Причини:

  1. Гарантія іммутабельності — якби можна було розширити Record, підклас міг би додати mutable поля
  2. Передбачувана поведінкаequals(), hashCode() завжди працюють однаково
  3. Оптимізація JVM — JIT може робити припущення про структуру
  4. Value types сумісність — підготовка до Project Valhalla

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

Замість наслідування — композиція:

// ❌ Не можна
public record AuditedRecord(String name) extends BaseEntity {}

// ✅ Можна через композицію
public record BaseEntity(LocalDateTime createdAt, String createdBy) {}
public record User(String name, BaseEntity audit) {}

// Або через інтерфейс
public interface Auditable {
    LocalDateTime createdAt();
    String createdBy();
}

public record User(String name, LocalDateTime createdAt, String createdBy) implements Auditable {}

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

  1. Спроба використовувати з Hibernate: ```java // ❌ JPA вимагає наслідування від базового entity @Entity public record User(Long id, String name) {} // не працює

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


2. **Очікування поліморфізму:**
```java
// Record не підтримує поліморфізм через наслідування класів, але підтримує
// через інтерфейси та sealed types.
public record Circle(double radius) {}
public record Rectangle(double w, double h) {}

// ✅ Використовуйте sealed interfaces + pattern matching (Java 21+)
public sealed interface Shape permits Circle, Rectangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double w, double h) implements Shape {}

double area(Shape s) {
    return switch (s) {
        case Circle c -> Math.PI * c.radius() * c.radius();
        case Rectangle r -> r.w() * r.h();
    };
}

🔴 Senior Level

Internal Implementation

Class file structure:

// Record має ACC_FINAL | ACC_RECORD прапорці
// super_class завжди вказує на java/lang/Record

ClassFile {
    access_flags: ACC_FINAL | ACC_RECORD  // implicit final
    super_class: constant_pool[java/lang/Record]
    interfaces_count: N  // може імплементувати інтерфейси
}

JVM specification (JVMS 4.7.31 — Record attribute та JVMS 4.1 — access_flags ACC_RECORD):

  • Record повинен мати ACC_FINAL прапорець
  • Super class повинен бути java.lang.Record
  • Не можна змінити через bytecode manipulation

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

Чому не можна розширювати:

1. Порушення інкапсуляції — підклас може змінити поведінку equals/hashCode
2. Проблеми з hashCode контрактом — різні підкласи можуть мати різні поля
3. Серіалізація — складно визначити canonical form для ієрархії
4. Pattern matching — деструктуризація вимагає фіксованої структури

Edge Cases

1. Sealed interfaces як альтернатива:

public sealed interface Event permits UserCreated, UserDeleted, OrderPlaced {}

public record UserCreated(String userId, Instant timestamp) implements Event {}
public record UserDeleted(String userId, String reason) implements Event {}
public record OrderPlaced(String orderId, BigDecimal amount) implements Event {}

// Pattern matching для обробки
String process(Event event) {
    return switch (event) {
        case UserCreated uc -> "User created: " + uc.userId();
        case UserDeleted ud -> "User deleted: " + ud.userId();
        case OrderPlaced op -> "Order placed: " + op.orderId();
    };
}

2. Generic Records:

public record ApiResponse<T>(int status, T data, String message) {}

// Спеціалізація через інтерфейси
public interface Validated {}
public record ValidatedResponse<T>(ApiResponse<T> response, boolean isValid)
    implements Validated {}

3. Composition over inheritance:

public record Pagination(int page, int size) {}
public record Sort(String field, Direction direction) {}

public record PagedRequest(Pagination pagination, Sort sort, String filter) {}

// Замість:
// public class PagedRequest extends Pagination { Sort sort; String filter; }

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

Наслідування vs Композиція:
- Наслідування: трохи швидший доступ до полів (один рівень)
- Композиція: negligible overhead (додаткове посилання)
- JIT інлайнить обидва підходи ефективно
- Різниця < 1% в реальних додатках

Production Experience

Реальний кейс — Event Sourcing:

// Event-driven архітектура з sealed + records
public sealed interface DomainEvent {
    UUID eventId();
    Instant occurredAt();
}

public record UserCreatedEvent(
    UUID eventId, Instant occurredAt, String userId, String name
) implements DomainEvent {}

public record UserEmailChangedEvent(
    UUID eventId, Instant occurredAt, String userId, String oldEmail, String newEmail
) implements DomainEvent {}

// Обробка
public void handle(DomainEvent event) {
    switch (event) {
        case UserCreatedEvent e -> createUser(e.userId(), e.name());
        case UserEmailChangedEvent e -> updateEmail(e.userId(), e.newEmail());
    }
}

Best Practices

// ✅ Sealed interfaces + records для поліморфізму
public sealed interface Payment permits CashPayment, CardPayment, CryptoPayment {}
public record CashPayment(BigDecimal amount) implements Payment {}
public record CardPayment(BigDecimal amount, String cardNumber) implements Payment {}
public record CryptoPayment(BigDecimal amount, String walletAddress) implements Payment {}

// ✅ Композиція для повторного використання
public record AuditInfo(LocalDateTime createdAt, String createdBy) {}
public record User(String name, String email, AuditInfo audit) {}

// ❌ Не намагайтеся емулювати наслідування
// ❌ Не використовуйте Record для JPA entities
// ❌ Не використовуйте Record для mutable state

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

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

  • Record не можна розширити — implicit final (ACC_FINAL прапорець)
  • Record не може наслідувати інший клас — завжди extends java.lang.Record
  • Record може імплементувати інтерфейси: record User implements Serializable
  • Альтернатива наслідуванню — композиція або sealed interfaces
  • Sealed interfaces + Records + pattern matching = заміна поліморфізму
  • JVM забороняє extends Record на рівні bytecode (ACC_RECORD прапорець)

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

  • Чому Record не можна розширити? — Для гарантії іммутабельності, передбачуваного equals/hashCode та оптимізації JVM
  • Як реалізувати поліморфізм з Records? — Sealed interfaces + pattern matching (Java 21+)
  • Чи можна використовувати Record з Hibernate? — Ні, JPA вимагає наслідування і mutable state
  • Чим композиція краща за наслідування для Records? — Зберігає іммутабельність, не порушує контракт Record

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

  • ❌ “Можна розширити Record через проміжний клас” — Заборонено на рівні JVM
  • ❌ “Record наслідує Object” — Record наслідує java.lang.Record
  • ❌ “Record підтримує поліморфізм через наслідування” — Тільки через інтерфейси та sealed types
  • ❌ “Можна використовувати Record для JPA entity ієрархії” — JPA не підтримує Records

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

  • [[1. Що таке Record в Java і з якої версії вони доступні]]
  • [[2. У чому основні відмінності Record від звичайного класу]]
  • [[4. Чи можна додавати додаткові методи в Record]]
  • [[17. Що таке PECS (Producer Extends Consumer Super)]]