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

Чи можна перевизначити конструктор в Record

Structured Java interview answer with junior, middle, and senior-level explanation.

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

🟢 Junior Level

Так, але з обмеженнями. В Record можна додати свій конструктор, але він зобов’язаний викликати канонічний конструктор (через this(...)).

Два способи:

// Спосіб 1: Повна форма
public record User(String name, int age) {
    // Свій конструктор — повинен викликати this(...)
    public User(String name) {
        this(name, 0);  // виклик канонічного конструктора
    }
}

// Спосіб 2: Компактний конструктор (тільки валідація)
public record User(String name, int age) {
    public User {
        if (age < 0) {
            throw new IllegalArgumentException("Invalid age");
        }
    }
}

Не можна:

  • ❌ Створити конструктор, який не викликає this(...)
  • ❌ Додати конструктор без виклику канонічного

🟡 Middle Level

Як це працює

1. Канонічний конструктор (автогенерований):

public record Point(int x, int y) {}

// Автогенерований:
public Point(int x, int y) {
    this.x = x;
    this.y = y;
}

2. Додатковий конструктор:

public record Point(int x, int y) {
    // Повинен викликати this(...) в першому рядку
    public Point() {
        this(0, 0);  // ✅ OK
    }

    public Point(int value) {
        this(value, value);  // ✅ OK
    }

    // ❌ public Point(String s) { } — немає this()!
}

3. Компактний конструктор (тільки валідація):

public record Email(String value) {
    // Компактний конструктор — НЕ містить сигнатури
    public Email {
        if (value == null || !value.contains("@")) {
            throw new IllegalArgumentException("Invalid email");
        }
    }
}

// Використання
Email e1 = new Email("test@example.com");  // ✅ OK
Email e2 = new Email("invalid");  // throws IllegalArgumentException

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

  1. Забули викликати this(...):
    public record User(String name) {
     // ❌ compilation error — немає виклику this()
     public User() {
         System.out.println("Created");
     }
    
     // ✅ Правильно
     public User() {
         this("Anonymous");
     }
    }
    
  2. Компактний конструктор з присвоюванням:
    public record User(String name) {
     // Компактний конструктор МОЖЕ присвоювати параметри:
     // name = name.toUpperCase() — валідно в Java 16+.
     // Присвоювання відбувається перед implicit field assignment.
     public User {
         name = name.toUpperCase();  // OK в Java 16+
     }
    }
    

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

1. Валідація:

public record Money(BigDecimal amount, Currency currency) {
    public Money {
        Objects.requireNonNull(currency, "Currency cannot be null");
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Negative amount");
        }
    }
}

2. Нормалізація даних:

public record Email(String value) {
    public Email {
        value = value.toLowerCase().trim();  // нормалізація
    }
}

3. Фабричні конструктори:

public record Range(int min, int max) {
    public Range(int value) {
        this(value, value);
    }

    public Range(String rangeStr) {
        // parsing "1-100" -> min=1, max=100
        String[] parts = rangeStr.split("-");
        this(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
    }
}

🔴 Senior Level

Internal Implementation

Компілятор генерує:

// Вихідний код
public record User(String name, int age) {
    public User {
        if (age < 0) throw new IllegalArgumentException();
    }

    public User(String name) {
        this(name, 0);
    }
}

// Десугарized:
public final class User extends Record {
    private final String name;
    private final int age;

    // Канонічний конструктор + компактна валідація
    public User(String name, int age) {
        if (age < 0) throw new IllegalArgumentException();
        this.name = name;
        this.age = age;
    }

    // Додатковий конструктор
    public User(String name) {
        this(name, 0);
    }
}

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

Компактний конструктор vs повний конструктор:

Підхід Плюси Мінуси
Компактний Короткий, тільки валідація Не можна змінити параметри
Повний Гнучкість, можна змінювати параметри Більше коду, ризик помилок

Edge Cases

1. Ланцюжок конструкторів:

public record Config(String host, int port, String protocol) {
    public Config(String host, int port) {
        this(host, port, "http");
    }

    public Config(String host) {
        this(host, 8080);
    }

    public Config() {
        this("localhost");
    }

    public Config {
        if (port < 1 || port > 65535) {
            throw new IllegalArgumentException("Invalid port");
        }
    }
}

2. Нормалізація з mutable об’єктами:

public record User(List<String> emails) {
    public User {
        // Defensive copy для mutable колекцій
        emails = new ArrayList<>(emails);
    }
}

3. Конструктор з винятком:

public record JsonNode(String json) {
    public JsonNode {
        try {
            // Валідація JSON
            Json.parse(json);
        } catch (JsonParseException e) {
            throw new IllegalArgumentException("Invalid JSON", e);
        }
    }
}

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

Конструктори Records:
- Компактний конструктор: inline валідація
- Повний конструктор: делегування (один виклик)
- Різниця negligible (< 1 ns)
- JIT інлайнить обидва підходи

Production Experience

DTO з валідацією:

public record CreateUserRequest(
    @NotBlank String name,
    @Email String email,
    @Min(18) int age
) {
    public CreateUserRequest {
        // Додаткова валідація після Bean Validation
        if (name.length() > 100) {
            throw new IllegalArgumentException("Name too long");
        }
    }
}

Domain objects:

public record OrderId(String value) {
    private static final Pattern UUID_PATTERN =
        Pattern.compile("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$");

    public OrderId {
        if (!UUID_PATTERN.matcher(value).matches()) {
            throw new IllegalArgumentException("Invalid UUID format");
        }
        value = value.toLowerCase();  // нормалізація
    }

    public static OrderId generate() {
        return new OrderId(UUID.randomUUID().toString());
    }
}

Best Practices

// ✅ Компактний конструктор для валідації
public record Email(String value) {
    public Email {
        Objects.requireNonNull(value);
        if (!value.contains("@")) throw new IllegalArgumentException();
    }
}

// ✅ Повний конструктор для зручності
public record Point(int x, int y) {
    public Point() { this(0, 0); }
    public Point(int v) { this(v, v); }
}

// ❌ Не мутуйте поля після присвоювання
// ❌ Не намагайтеся обійти валідацію
// ❌ Не додавайте побічні ефекти в конструктор

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

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

  • В Record можна додати свій конструктор, але він зобов’язаний викликати канонічний через this(...)
  • Компактний конструктор — форма без сигнатури, тільки для валідації та нормалізації
  • Компактний конструктор вбудовується в канонічний перед присвоюванням полів
  • В компактному конструкторі можна змінювати параметри перед присвоюванням: name = name.toUpperCase()
  • Checked exceptions не можна викинути з компактного конструктора (canonical не declared throws)
  • Можна мати кілька додаткових конструкторів + один компактний

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

  • Чим компактний конструктор відрізняється від звичайного? — Компактний не має сигнатури, тільки валідація; звичайний повинен викликати this(...)
  • Чи можна нормалізувати дані в конструкторі? — Так, в компактному можна: name = name.trim()
  • Що буде якщо конструктор не викличе this(…)? — Compilation error: “constructor must call this(…)”
  • Чи можна викинути checked exception з компактного конструктора? — Ні, потрібно загорнути в unchecked

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

  • ❌ “Компактний конструктор присвоює поля через this.field” — Використовується ім’я параметра, не this.field
  • ❌ “Можна створити конструктор без виклику this()” — Обов’язковий виклик канонічного конструктора
  • ❌ “Компактний конструктор може додати поле” — Не можна додати instance поле
  • ❌ “Конструктор може викинути checked exception” — Канонічний конструктор не declared throws

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

  • [[1. Що таке Record в Java і з якої версії вони доступні]]
  • [[4. Чи можна додавати додаткові методи в Record]]
  • [[7. Що таке компактний конструктор (compact constructor) в Record]]
  • [[9. Чи є поля Record фінальними]]