Вопрос 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) {
     // Компактный конструктор МОЖЕТ переassign-ить параметры:
     // 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 финальными]]