Вопрос 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 финальными]]