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

Що таке компактний конструктор (compact constructor) в Record

Він не містить сигнатури — компілятор сам розуміє, що це компактний конструктор.

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

🟢 Junior Level

Компактний конструктор — це спеціальна форма конструктора в Record, яка використовується тільки для валідації та нормалізації даних.

Він не містить сигнатури — компілятор сам розуміє, що це компактний конструктор.

public record User(String name, int age) {
    // Компактний конструктор — тільки тіло, без сигнатури
    public User {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
    }
}

Відмінності від звичайного конструктора:

// Звичайний конструктор
public User(String name, int age) {
    this.name = name;
    this.age = age;
}

// Компактний конструктор (тільки валідація)
public User {
    if (age < 0) {
        throw new IllegalArgumentException();
    }
}

🟡 Middle Level

Як це працює

Компактний конструктор вбудовується в канонічний конструктор автоматично:

public record Email(String value) {
    public Email {
        // Цей код виконається ПЕРЕД присвоюванням полів
        if (value == null || !value.contains("@")) {
            throw new IllegalArgumentException("Invalid email");
        }
    }
}

// Компілятор генерує:
public Email(String value) {
    // Код з компактного конструктора
    if (value == null || !value.contains("@")) {
        throw new IllegalArgumentException("Invalid email");
    }
    // Потім присвоювання
    this.value = value;
}

Важливо: Параметри в компактному конструкторі не фінальні — можна змінити значення перед присвоюванням!

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

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

  1. Спроба присвоїти this.value:
    public record User(String name) {
     public User {
         // ❌ Не можна використовувати this.value
         this.name = name.toUpperCase();  // compilation error
    
         // ✅ Використовуйте просто name
         name = name.toUpperCase();  // OK — це параметр, не поле
     }
    }
    
  2. Спроба додати поля:
    public record User() {
     public User {
         // ❌ Не можна додати поле
         int extra = 0;  // compilation error
     }
    }
    

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

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

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

2. Нормалізація:

public record PhoneNumber(String value) {
    public PhoneNumber {
        // Прибираємо все крім цифр
        value = value.replaceAll("[^0-9+]", "");
    }
}

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

public record TagCloud(List<String> tags) {
    public TagCloud {
        // Defensive copy
        tags = new ArrayList<>(tags);
        // Нормалізація
        tags.replaceAll(String::toLowerCase);
        tags.sort(null);
    }
}

🔴 Senior Level

Internal Implementation

Десугаризація:

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

// Десугаризований код
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();
        name = name.toUpperCase();

        // Присвоювання полів (генерується компілятором)
        this.name = name;
        this.age = age;
    }
}

Обмеження:

  • Параметри компактного конструктора — effectively final, але не final
  • Можна змінити параметри перед присвоюванням
  • Не можна звертатися до this до присвоювання // Оскільки сигнатура канонічного конструктора не містить throws, // checked exception потрібно загорнути в unchecked.
  • Не можна викинути checked exception (бо канонічний конструктор не declared throws)

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

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

Аспект Компактний Повний
Валідація ✅ Ідеальний ✅ Підходить
Нормалізація ✅ Підходить ✅ Підходить
Зміна параметрів ✅ Можна ✅ Можна
Делегування ❌ Не можна ✅ Можна
Кілька конструкторів ❌ Один ✅ Кілька

Edge Cases

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

public record DateRange(LocalDate start, LocalDate end) {
    public DateRange {
        Objects.requireNonNull(start);
        Objects.requireNonNull(end);
        if (start.isAfter(end)) {
            // Авто-корекція
            var temp = start;
            start = end;
            end = temp;
        }
    }
}

2. Компактний конструктор з checked exception:

public record JsonData(String json) {
    public JsonData {
        try {
            Json.parse(json);
        } catch (JsonParseException e) {
            // ❌ Не можна викинути checked exception
            // ✅ Wrap в unchecked
            throw new IllegalArgumentException("Invalid JSON", e);
        }
    }
}

3. Компактний конструктор + додатковий конструктор:

public record Point(int x, int y) {
    // Компактний — для валідації
    public Point {
        if (x < 0 || y < 0) {
            throw new IllegalArgumentException("Negative coordinates");
        }
    }

    // Додатковий — для зручності
    public Point(int value) {
        this(value, value);  // викликає канонічний (з валідацією)
    }
}

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

Компактний конструктор:
- Zero overhead — код inline в канонічний конструктор
- JIT може оптимізувати валідацію
- Жодних додаткових викликів

Production Experience

Value objects з інваріантами:

public record Range(int min, int max) {
    public Range {
        if (min > max) {
            throw new IllegalArgumentException(
                "Min (%d) cannot be greater than max (%d)".formatted(min, max));
        }
    }

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

// Використання
Range r = new Range(1, 10);  // OK
Range bad = new Range(10, 1);  // throws IllegalArgumentException

DDD Value Objects:

public record UserId(UUID value) {
    public UserId {
        Objects.requireNonNull(value, "UserId cannot be null");
    }

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

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

public record Email(String value) {
    private static final Pattern EMAIL_PATTERN =
        Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$");

    public Email {
        Objects.requireNonNull(value, "Email cannot be null");
        value = value.toLowerCase().trim();
        if (!EMAIL_PATTERN.matcher(value).matches()) {
            throw new IllegalArgumentException("Invalid email format");
        }
    }
}

Best Practices

// ✅ Використовуйте компактний конструктор для валідації
public record Age(int value) {
    public Age {
        if (value < 0 || value > 150) {
            throw new IllegalArgumentException("Invalid age");
        }
    }
}

// ✅ Для нормалізації mutable об'єктів
public record Tags(Set<String> tags) {
    public Tags {
        tags = Set.copyOf(tags);  // immutable copy
    }
}

// ❌ Не використовуйте для складної логіки
// ❌ Не викидайте checked exceptions
// ❌ Не звертайтеся до this до присвоювання

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

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

  • Компактний конструктор — форма без сигнатури, тільки для валідації та нормалізації
  • Код компактного конструктора вбудовується в канонічний ПЕРЕД присвоюванням полів
  • Параметри можна змінювати: value = value.toLowerCase().trim() — це змінить значення до присвоювання
  • Не можна використовувати this.field = ... — тільки ім’я параметра
  • Checked exceptions не можна викинути напряму (canonical конструктор не declared throws)
  • Не можна додати поля в компактному конструкторі

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

  • Навіщо потрібен компактний конструктор? — Для валідації та нормалізації даних без boilerplate
  • Чи можна змінити значення параметра? — Так, це normal practice: value = value.trim()
  • Чому не можна використовувати this.field? — Присвоювання полів генерується автоматично після компактного конструктора
  • Чи можна викинути checked exception? — Ні, потрібно загорнути в unchecked (IllegalArgumentException)

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

  • ❌ “Компактний конструктор присвоює поля” — Присвоювання автоматичне, тільки параметри можна змінювати
  • ❌ “Компактний конструктор має сигнатуру” — Немає сигнатури, компілятор визначає за формою
  • ❌ “Можна створити додаткове поле” — Instance поля заборонені в Record
  • ❌ “Можна викинути IOException з компактного конструктора” — Тільки unchecked exceptions

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

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