Перейти к содержимому
Теория · Интерфейсы, enum и record · урок 7 из 8

Записи record

Представьте бланк заявления с графами «имя», «дата» и «подпись». Заполненный и сданный бланк никто не правит: если нужно что-то изменить, заполняют новый. Зато два бланка можно сравнить по графам: если в каждой графе написано одно и то же, заявления одинаковые.

Вспомните класс Point из темы о классах. Чтобы точку можно было создать сразу с координатами, напечатать и сравнить с другой, в класс пришлось бы дописать конструктор, методы для чтения полей, toString и метод сравнения. Это десятки строк, хотя сама точка — всего два числа. Для классов, которые только хранят данные, в Java 16 появились записи:

JAVA
public class Main {
    public static void main(String[] args) {
        Point a = new Point(3, 4);
        Point b = new Point(3, 4);
        System.out.println(a);           // Point[x=3, y=4]
        System.out.println(a.x());       // 3
        System.out.println(a == b);      // false
        System.out.println(a.equals(b)); // true
    }
}

record Point(int x, int y) {}

Давайте разберём программу по шагам:

  1. record Point(int x, int y) {} — всё объявление. После имени в скобках перечислены компоненты — графы бланка, а тело в фигурных скобках пустое.
  2. new Point(3, 4) вызывает конструктор, хотя его никто не писал.
  3. println(a) печатает Point[x=3, y=4]: имя записи и каждую графу.
  4. a.x() читает компонент x. Это метод, и называется он как компонент, без get.
  5. a == b даёт false: это два разных объекта, два отдельных бланка.
  6. a.equals(b) даёт true: записи сравниваются по компонентам, и в обеих лежат 3 и 4.

Что пишет компилятор

Запись — класс, в котором вы объявляете только данные, а остальное за вас пишет компилятор. Строка record Point(int x, int y) {} даёт:

  • поля private final int x и private final int y;
  • конструктор с параметрами x и y в том же порядке;
  • методы чтения x() и y();
  • toString в виде Point[x=3, y=4];
  • equals, который сравнивает компоненты по очереди, и hashCode, который даёт одинаковое число равным записям.

Этот вопрос часто задают на собеседовании: чем запись отличается от обычного класса? Ответ: запись хранит неизменяемые данные, а конструктор, методы чтения, equals, hashCode и toString для неё пишет компилятор.

Запись может реализовать интерфейс, как обычный класс. Но наследовать другой класс она не может, и сама она неявно final, поэтому наследовать от неё нельзя.

Запись нельзя изменить

Поля записи final: после конструктора их не меняет никто, бланк сдан. Чтобы «изменить» точку, создают новую:

JAVA
Point p = new Point(3, 4);
p = new Point(5, p.y());
System.out.println(p); // Point[x=5, y=4]

Вторая строка строит новую запись: x в ней равен 5, а y перенесён из старой через p.y(). Переменная p теперь ведёт к новому объекту, а старая точка осталась прежней.

Здесь картинка с бланком неточна. Если компонент записи — массив, в графе лежит не ряд шкафчиков, а талон с его адресом. Талон заменить нельзя, а вот содержимое шкафчиков поменять можно. Неизменяемы только сами графы.

Компактный конструктор

Иногда данные нужно проверить или поправить, пока бланк ещё не сдан. Для этого в записи пишут компактный конструктор — без списка параметров, только имя записи и тело:

JAVA
public class Main {
    public static void main(String[] args) {
        System.out.println(new Range(5, 1)); // Range[from=1, to=5]
    }
}

record Range(int from, int to) {
    Range {
        if (from > to) {
            int t = from;
            from = to;
            to = t;
        }
    }
}

Разберём, как он работает:

  1. Range { … } — компактный конструктор: его параметры from и to те же, что у записи.
  2. Если from больше to, тело меняет параметры местами через переменную t.
  3. После тела компилятор сам добавляет присваивания this.from = from; и this.to = to;, поэтому в поля попадают уже исправленные значения.
  4. new Range(5, 1) печатается как Range[from=1, to=5].

Частая ошибка

Координату записи попытались поменять:

JAVA
Point p = new Point(3, 4);
p.x = 5; // ✗
TEXT
error: x has private access in Point

Программа не компилируется. Поля записи закрыты словом private, поэтому снаружи их читают только через метод p.x(). Внутри записи присвоить полю тоже не выйдет: оно final, и компилятор ответит cannot assign a value to final variable x. Чтобы получить точку с другим x, создайте новую запись, как в разделе выше.

Главное

  • record Point(int x, int y) {} объявляет данные, а конструктор, методы x() и y(), toString, equals и hashCode пишет компилятор.
  • equals записи сравнивает компоненты: две записи с одинаковыми данными равны, хотя == для них даёт false.
  • Поля записи private final: запись не меняют, а создают новую.
  • Компактный конструктор проверяет или поправляет значения до того, как они попадут в поля.