☕ Вопросы интервью по Java

Вопросы 221-230 - Records

Вопрос 221
Что такое record в Java и зачем он нужен?

Ответ: Record (запись) — особый вид класса для неизменяемых данных, появился в Java 16 (в 14 и 15 был в режиме preview). Вы перечисляете компоненты, а компилятор сам генерирует конструктор, методы доступа, equals(), hashCode() и toString().

Сравнение:

// Раньше: около 30 строк
public final class Point {
    private final int x;
    private final int y;
    public Point(int x, int y) { this.x = x; this.y = y; }
    public int x() { return x; }
    public int y() { return y; }
    // + equals, hashCode, toString
}

// Теперь
public record Point(int x, int y) { }

Point p = new Point(1, 2);
System.out.println(p);        // Point[x=1, y=2]
System.out.println(p.x());    // 1
                
💡 Важно: record — это «прозрачный носитель данных» (transparent data carrier): его состояние полностью описывается компонентами в заголовке.
Вопрос 222
Что компилятор автоматически генерирует для record?

Ответ:

Что Детали
Поля private final поле для каждого компонента
Канонический конструктор Принимает все компоненты в порядке объявления
Методы доступа Называются как компонент: p.x(), а не getX()
equals() / hashCode() Два record равны, если они одного типа и все компоненты равны
toString() Формат Point[x=1, y=2]

Любой из этих методов можно переопределить вручную.


Point a = new Point(1, 2);
Point b = new Point(1, 2);
System.out.println(a == b);        // false (разные объекты)
System.out.println(a.equals(b));   // true  (равны по значению)
                
Вопрос 223
Что такое компактный конструктор?

Ответ: Компактный конструктор объявляется без списка параметров и нужен для проверки или нормализации значений. Присваивание полям компилятор добавляет сам в конце.


public record Range(int from, int to) {
    public Range {                       // без скобок и параметров
        if (from > to) {
            throw new IllegalArgumentException("from > to");
        }
        // можно изменить параметры до автоматического присваивания:
        // from = Math.max(from, 0);
    }
}

new Range(5, 1);   // IllegalArgumentException
                
Правила:
  • В компактном конструкторе нельзя писать this.from = from — ошибка компиляции
  • Можно объявить канонический конструктор полностью (с параметрами), тогда присваивать поля нужно самому
  • Любой дополнительный конструктор обязан первой строкой вызывать другой через this(...)

public record Range(int from, int to) {
    public Range(int to) {
        this(0, to);          // делегирует каноническому
    }
}
                
Вопрос 224
Какие ограничения есть у record?

Ответ:

  • Record неявно final — наследоваться от него нельзя
  • Record не может расширять другой класс (он уже неявно наследует java.lang.Record)
  • Нельзя объявлять дополнительные экземплярные поля, только компоненты из заголовка
  • Поля компонентов final — менять их нельзя, сеттеров нет
  • Нельзя сделать abstract
Что разрешено:
  • Реализовывать интерфейсы
  • Объявлять обычные методы, статические поля и методы, вложенные типы
  • Быть обобщённым: record Pair<A, B>(A first, B second) { }
  • Объявляться локально внутри метода
  • Использовать аннотации на компонентах

public record Circle(double radius) implements Shape {
    static final double PI = Math.PI;          // статическое поле можно

    public double area() {                     // метод можно
        return PI * radius * radius;
    }
    // private int cache;                      // ОШИБКА: экземплярное поле
}
                
Вопрос 225
Record неизменяемый (immutable) полностью?

Ответ: Нет, он неглубоко неизменяем (shallowly immutable). Ссылки на компоненты изменить нельзя, но если компонент — изменяемый объект, то его содержимое менять можно.


record Team(String name, List<String> members) { }

List<String> list = new ArrayList<>(List.of("Ann"));
Team t = new Team("A", list);

list.add("Bob");
System.out.println(t.members());   // [Ann, Bob] — record «изменился»
                
Защитное копирование в компактном конструкторе:

record Team(String name, List<String> members) {
    Team {
        members = List.copyOf(members);   // неизменяемая копия, null-элементы запрещены
    }
}
                
💡 Важно: метод доступа тоже возвращает ту же ссылку, поэтому при изменяемых компонентах делайте копию и в конструкторе, и при необходимости в аксессоре.
Вопрос 226
Чем record отличается от обычного класса и от Lombok @Value?

Ответ:

Критерий Record Lombok @Value
Реализация Возможность языка Внешняя библиотека с процессором аннотаций
Имена аксессоров name() getName()
Наследование Невозможно Класс final по умолчанию
Дополнительные поля Запрещены Разрешены
Pattern matching Поддерживает деконструкцию (record patterns) Нет
Зависимости Не нужны Нужен Lombok и поддержка в IDE
💡 Совет: для простых неизменяемых DTO и значений выбирайте record. Класс остаётся нужным, если требуются наследование, изменяемое состояние или скрытые поля.
Вопрос 227
Где обычно используют record?

Ответ:

  • DTO — объекты запросов и ответов REST API (Jackson 2.12+ умеет сериализовать и десериализовать record)
  • Составные ключи в Map: equals/hashCode уже корректны
  • Возврат нескольких значений из метода вместо Object[] или Pair
  • Промежуточные результаты в stream-операциях (локальный record)
  • Value objects предметной области: Money, Email, Coordinates
  • Spring Data — DTO-проекции запросов и записи для @ConfigurationProperties

// локальный record в stream
record NameLen(String name, int len) { }

List<NameLen> result = names.stream()
    .map(n -> new NameLen(n, n.length()))
    .filter(r -> r.len() > 3)
    .toList();
                
Где не подходит:
  • JPA-сущности (@Entity) — им нужны конструктор без аргументов, не-final класс и изменяемые поля
  • Объекты со сложным изменяемым состоянием и поведением
Вопрос 228
Что такое record patterns (деконструкция record)?

Ответ: Record pattern позволяет в instanceof и switch одновременно проверить тип и достать компоненты. В финальном виде появился в Java 21.


record Point(int x, int y) { }

Object o = new Point(3, 4);

// до Java 21
if (o instanceof Point p) {
    System.out.println(p.x() + p.y());
}

// record pattern
if (o instanceof Point(int x, int y)) {
    System.out.println(x + y);        // 7
}
                
Вложенные шаблоны:

record Line(Point from, Point to) { }

static int dx(Object o) {
    if (o instanceof Line(Point(var x1, var y1), Point(var x2, var y2))) {
        return x2 - x1;
    }
    return 0;
}
                
Вопрос 229
Как record работают с сериализацией и рефлексией?

Ответ:

Сериализация (Serializable):
  • Сериализуется только состояние компонентов, writeObject/readObject игнорируются
  • При десериализации вызывается канонический конструктор, поэтому проверки из компактного конструктора выполняются и невалидный объект не получится создать через поток байтов
Рефлексия:

record Point(int x, int y) { }

Point.class.isRecord();                        // true
for (RecordComponent c : Point.class.getRecordComponents()) {
    System.out.println(c.getName() + " : " + c.getType());
}
// x : int
// y : int

Field f = Point.class.getDeclaredField("x");
f.setAccessible(true);
f.set(new Point(1, 2), 5);    // IllegalAccessException: поля record изменить нельзя
                
💡 Важно: метод Class.getRecordComponents() и isRecord() появились в Java 16.
Вопрос 230
Можно ли переопределить методы доступа и добавить свою логику?

Ответ: Да. Можно переопределить любой аксессор, equals, hashCode и toString, но сигнатура аксессора должна совпадать (public, без параметров, тот же тип возврата).


record User(String name, String email) {

    @Override
    public String name() {              // нормализуем при чтении
        return name.trim();
    }

    @Override
    public String toString() {
        return "User(" + name + ")";    // не выводим email в логи
    }

    // производный метод, не компонент
    public String domain() {
        return email.substring(email.indexOf('@') + 1);
    }
}
                
💡 Важно: если переопределяете equals или hashCode, сохраняйте контракт: равные объекты должны иметь равные хеши. Обычно это не нужно — генерируемые версии уже верны.