W3docs

Java Records

Используйте Java records для создания компактных неизменяемых классов с автогенерируемыми аксессорами, equals и hashCode.

Record — это класс, единственная задача которого состоит в хранении данных. Вы объявляете поля один раз в заголовке, а компилятор генерирует конструктор, аксессоры, equals, hashCode и toString за вас. То, что раньше требовало 40 строк геттеров и шаблонного кода, теперь умещается в одну строку:

public record Point(int x, int y) {}

Это весь класс. new Point(3, 4), p.x(), p.y(), p.equals(other) и p.toString() — всё работает именно так, как вы ожидаете.

Records были финализированы в Java 16 (после предварительных версий в 14 и 15). На этой странице рассматривается то, что генерирует компилятор, как проводить валидацию и настраивать record, правила, которые records соблюдают, и как records взаимодействуют с сопоставлением с образцом.

Зачем нужны records

До появления records каждый «класс-носитель данных» требовал по одному приватному финальному полю на компонент, конструктора, который их присваивает, геттера на каждое поле, основанных на значениях equals и hashCode, а также toString. Большая часть этого кода была механической и легко поддавалась незаметным ошибкам (забытое поле в equals, неправильное поле в геттере, опечатка при копировании). Records сворачивают всё это в заголовок, устраняя как набор кода, так и баги.

Компоненты и аксессоры

Аргументы в заголовке называются компонентами. Каждый из них становится:

  • Полем private final с тем же именем.
  • Методом-аксессором с тем же именем (не getX() — просто x()).
Point p = new Point(3, 4);
System.out.println(p.x() + ", " + p.y());   // 3, 4

Имена совпадают, потому что records предназначены для открытого представления данных. Префикс get отсутствует, поскольку скрывать нечего.

Генерируемые equals, hashCode, toString

Два record'а равны тогда и только тогда, когда они одного типа и каждый компонент равен соответствующему:

Point a = new Point(3, 4);
Point b = new Point(3, 4);
System.out.println(a.equals(b));  // true
System.out.println(a);            // Point[x=3, y=4]

hashCode объединяет все компоненты, поэтому records корректно работают в качестве ключей в HashMap и HashSet без каких-либо дополнительных усилий.

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

Каждый record имеет канонический конструктор — тот, параметры которого совпадают с компонентами по порядку. По умолчанию компилятор пишет его за вас (он просто присваивает каждый параметр соответствующему полю). Когда необходима валидация или нормализация, не нужно повторять эти присваивания: напишите компактный конструктор, который не имеет списка параметров и выполняется до неявного присваивания полей:

public record Range(int low, int high) {
  public Range {
    if (low > high)
      throw new IllegalArgumentException("low > high");
  }
}

Внутри компактного конструктора также можно переприсваивать переменные параметров — финальные значения и будут присвоены полям:

public record Name(String first, String last) {
  public Name {
    first = first.strip();
    last  = last.strip();
  }
}

Добавление методов

Records могут содержать любые методы, которые вы обычно пишете, — они просто не могут иметь дополнительные поля экземпляра (всё, что лежит в основе record, должно быть компонентом):

public record Point(int x, int y) {
  public double distanceTo(Point other) {
    int dx = x - other.x;
    int dy = y - other.y;
    return Math.sqrt(dx * dx + dy * dy);
  }
}

Статические поля и статические методы допустимы. Records также могут реализовывать интерфейсы.

Что records не могут делать

  • Нет наследования. Record неявно расширяет java.lang.Record и является final — вы не можете расширить record, и record не может расширить другой класс.
  • Нет изменяемого состояния. Все компоненты являются final. Если вам нужна мутабельность, используйте обычный класс.
  • Нет полей экземпляра вне компонентов. Нельзя добавить скрытое private int cache;.

Эти ограничения и есть смысл records. Record обещает: «Я — только мои компоненты, и ничего больше». Именно это обещание делает безопасной автогенерацию equals, hashCode и сериализации.

Когда использовать record

Records подходят тогда, когда в противном случае вы бы написали небольшой неизменяемый класс-носитель данных — DTO, объекты конфигурации, возвращаемые типы, группирующие несколько значений, кортежи в switch с сопоставлением с образцом. Они не подходят, когда тип обладает идентичностью, владеет изменяемым состоянием или является корнем иерархии.

Практический пример

java— editable, runs on the server

Records в сопоставлении с образцом

Поскольку компоненты record'а публичны и упорядочены, компилятор также знает, как его разобрать. Образец record'а связывает каждый компонент с переменной за один шаг, что делает records естественной формой для данных, по которым выполняется switch:

sealed interface Shape permits Circle, Rect {}
record Point(int x, int y) {}
record Circle(Point center, double r) implements Shape {}
record Rect(Point a, Point b) implements Shape {}

static String describe(Shape s) {
  return switch (s) {
    case Circle(Point c, double r) -> "circle r=" + r + " at " + c.x() + "," + c.y();
    case Rect(Point a, Point b)    -> "rect " + a + " to " + b;
  };
}

Образец Circle(Point c, double r) проверяет, что s является Circle, и извлекает его компоненты в одном выражении. Подробнее об этой возможности, включая вложенные образцы, читайте в разделе сопоставление с образцом в Java.

Что дальше

Records фиксируют класс на определённом наборе данных. В следующей главе рассматриваются запечатанные классы (sealed classes), которые фиксируют иерархию на определённом наборе подтипов — недостающий элемент для моделирования замкнутых семейств алгебраических типов данных в Java. Продолжите с Java sealed classes.

Практика

Практика
Что компилятор генерирует при объявлении `record Point(int x, int y) {}`?
Что компилятор генерирует при объявлении `record Point(int x, int y) {}`?
Was this page helpful?