Вопрос 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]]