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

Чи можна додавати додаткові методи в Record

Record дозволяє додавати: 4. Приватні методи (для внутрішньої логіки) 5. Конструктори (повинні викликати канонічний)

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

🟢 Junior Level

Так, можна! Record — це не просто набір полів. Ви можете додавати будь-які методи, але з деякими обмеженнями.

Що можна:

public record Money(BigDecimal amount, String currency) {
    // ✅ Статичні методи
    public static Money zero() {
        return new Money(BigDecimal.ZERO, "USD");
    }

    // ✅ Додаткові методи
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Different currencies");
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }

    // ✅ Приватні методи (Java 16+)
    private void validate() {
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Negative amount");
        }
    }
}

Що не можна:

  • ❌ Додавати instance поля (крім static)
  • ❌ Робити Record mutable

🟡 Middle Level

Як це працює

Record дозволяє додавати:

  1. Статичні поля (static fields)
  2. Статичні методи (static methods)
  3. Instance методи (будь-якої видимості)
  4. Приватні методи (для внутрішньої логіки)
  5. Конструктори (повинні викликати канонічний)

Приклади:

public record User(String name, int age, String email) {
    // ✅ Статичне поле
    public static final int MAX_AGE = 150;

    // ✅ Статична фабрика
    public static User anonymous() {
        return new User("Anonymous", 0, "");
    }

    // ✅ Instance метод
    public boolean isAdult() {
        return age >= 18;
    }

    // ✅ Приватний метод
    private boolean isValidEmail() {
        return email != null && email.contains("@");
    }

    // ✅ Канонічний конструктор з валідацією
    public User {
        if (age < 0 || age > MAX_AGE) {
            throw new IllegalArgumentException("Invalid age");
        }
    }
}

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

  1. Спроба додати instance поле:
    public record BadRecord() {
     // ❌ Instance поле — помилка компіляції
     private int mutableField = 0;
    
     // ✅ Тільки static
     private static int counter = 0;
    }
    
  2. Зміна повертаного типу аксесора:
    public record Point(int x, int y) {
     // ❌ Не можна перевизначити аксесор з іншим типом
     public String x() {  // compilation error
         return String.valueOf(x);
     }
    }
    

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

1. Value objects з логікою:

public record Range(int min, int max) {
    public Range {
        if (min > max) {
            throw new IllegalArgumentException("min > max");
        }
    }

    public boolean contains(int value) {
        return value >= min && value <= max;
    }

    public Range intersect(Range other) {
        return new Range(
            Math.max(this.min, other.min),
            Math.min(this.max, other.max)
        );
    }
}

2. Builder-подібні методи:

public record Query(String table, List<String> columns, String where) {
    public Query(String table) {
        this(table, List.of("*"), null);
    }

    public Query select(String... columns) {
        return new Query(this.table, List.of(columns), this.where);
    }

    public Query where(String condition) {
        return new Query(this.table, this.columns, condition);
    }
}

// Використання
Query q = new Query("users")
    .select("name", "email")
    .where("age > 18");

🔴 Senior Level

Internal Implementation

Компілятор дозволяє:

  • Додаткові методи не впливають на canonical конструктор
  • Instance методи не змінюють структуру Record
  • Статичні поля зберігаються в класі, а не в екземплярах

Обмеження на перевизначення:

public record User(String name) {
    // Починаючи з Java 16+ МОЖНА перевизначити аксесор з тією ж сигнатурою.
    // В preview-версіях це було заборонено.
    public String name() { return name.toUpperCase(); }  // OK в Java 16+

    // ❌ Не можна перевизначити final методи з java.lang.Record
    // Методи equals/hashCode/toString declared як abstract в java.lang.Record,
    // але їх МОЖНА і потрібно перевизначати.
}

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

Методи в Record vs окремі утиліти:

Підхід Плюси Мінуси
Методи в Record Логіка поруч з даними, зручніше Record стає «товстішим»
Окремі утиліти Record залишається простим Більше файлів, імпортів

Edge Cases

1. Перевизначення equals/hashCode:

public record CaseInsensitiveString(String value) {
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof CaseInsensitiveString other)) return false;
        return Objects.equals(
            this.value.toLowerCase(),
            other.value.toLowerCase()
        );
    }

    @Override
    public int hashCode() {
        return value.toLowerCase().hashCode();
    }
}

2. Приватні конструктори:

public record UserId(UUID value) {
    // ✅ Приватний конструктор (повинен викликати канонічний)
    private UserId(String value) {
        this(UUID.fromString(value));
    }

    public static UserId of(String value) {
        return new UserId(value);
    }
}

3. Generic методи в Record:

public record JsonNode(String json) {
    public <T> T parse(Class<T> type) {
        // парсинг JSON
        return null;
    }
}

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

Додаткові методи:
- No overhead в пам'яті (методи в class, не в instance)
- JIT інлайнить методи як звичайно
- Різниці зі звичайним класом немає

Production Experience

Real-world приклад — Domain Events:

public record OrderCreatedEvent(
    String orderId,
    List<OrderItem> items,
    Instant createdAt
) {
    public BigDecimal totalAmount() {
        return items.stream()
            .map(item -> item.price().multiply(BigDecimal.valueOf(item.quantity())))
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    public boolean isHighValue() {
        return totalAmount().compareTo(new BigDecimal("1000")) > 0;
    }

    public record OrderItem(String productId, int quantity, BigDecimal price) {}
}

Best Practices

// ✅ Додавайте бізнес-логіку в Record
public record Money(BigDecimal amount, Currency currency) {
    public Money add(Money other) { /* ... */ }
    public boolean isPositive() { return amount.compareTo(BigDecimal.ZERO) > 0; }
}

// ✅ Статичні фабрики для зручності
public record Point(double x, double y) {
    public static Point fromPolar(double r, double theta) {
        return new Point(r * Math.cos(theta), r * Math.sin(theta));
    }
}

// ❌ Не робіть Record занадто «товстим»
// ❌ Не додавайте mutable стан
// ❌ Не перевизначайте аксесори

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

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

  • В Record можна додавати static поля, static методи, instance методи
  • Instance поля (не static) — заборонені, тільки final канонічні компоненти
  • Приватні методи підтримуються (Java 16+)
  • Конструктори повинні викликати канонічний через this(...)
  • Можна перевизначити equals/hashCode/toString, але не обов’язково
  • Аксесори можна перевизначити в Java 16+ (в preview було заборонено)

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

  • Чи можна додати поле в Record? — Тільки static, instance поля заборонені
  • Чи можна додати приватний метод? — Так, приватні методи повністю підтримуються
  • Чи можна перевантажити конструктор? — Так, але додатковий конструктор зобов’язаний викликати канонічний
  • Чи варто перевизначати equals/hashCode? — Тільки якщо потрібна кастомна логіка (case-insensitive тощо)

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

  • ❌ “В Record не можна додати жодного методу” — Можна static та instance методи
  • ❌ “Можна додати mutable поле в Record” — Тільки static поля, instance заборонені
  • ❌ “Методи в Record збільшують пам’ять об’єкта” — Методи в class, не в instance
  • ❌ “Не можна перевизначити аксесор” — Можна в Java 16+, але з тією ж сигнатурою

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

  • [[1. Що таке Record в Java і з якої версії вони доступні]]
  • [[5. Які методи автоматично генеруються для Record]]
  • [[6. Чи можна перевизначити конструктор в Record]]
  • [[8. Чи можна оголошувати статичні поля та методи в Record]]