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 с сопоставлением с образцом. Они не подходят, когда тип обладает идентичностью, владеет изменяемым состоянием или является корнем иерархии.
Практический пример
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.