Что такое Record в Java 16?
Record — это новый тип объявления (type of declaration) для определения неизменяемых (immutable) классов данных.
Применение Record вместо обычного класса позволяет избежать рутинного написания методов hashCode(), equals(), toString(), геттеров и конструктора.
Для тех, кто знаком с Project Lombok – это очень похоже на аннотацию @Data.
Использование нового типа объявления в ряде случаев может существенно повысить производительность разработки.
Рассмотрим это на простом примере:
class Customer {
private final int age;
private final String fullName;
Customer (int age, String fullName) {
this.age = age;
this.fullName = fullName;
}
public int getAge() {
return age;
}
public String getFullName () {
return fullName;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Customer c = (Customer) o;
return age == c.age && fullName.equals(c.fullName);
}
@Override
public int hashCode() {
return Objects.hash(age, fullName);
}
@Override
public String toString() {
return new StringJoiner(", ", Customer.class.getSimpleName() + "[", "]")
.add("age=" + age)
.add("fullName =" + fullName)
.toString();
}
}
С применением Record вышеуказанный пример превращается в всего лишь одну строчку:
record Customer (int age, String fullName) {}
Для чего было необходимо добавление Record в Java?
Основной причиной было улучшение читаемости кода, облегчение расширения и поддержки существующего кода.
Как уже упоминалось выше, очевидные преимущества применения Record:
- автоматическое создание публичного конструктора (auto-generated public constructor)
- автоматическое создание частных неизменяемых полей (private immutable (i.e. final) fields)
- автоматическое создание методов hashCode(), equals(), и toString()
- автоматическое создание геттеров
Все эти преимущества становятся особенно удобными, когда в существующую структуру данных добавляется новое поле или наоборот убирается из нее. В случае с Record нет необходимости изменять или переопределять конструктор, поля, геттеры и методы hashCode, equals, toString; всё будет выполнено автоматически без участия разработчика.
Давайте рассмотрим использование Record более подробно.
Конструктор
Тип Record по умолчанию имеет так называемый канонический конструктор - тот, который инициализирует все имеющиеся поля.
Мы можем расширить функционал канонического конструктора, добавив к нему желаемое поведение с помощью компактной записи этого конструктора, как в примере ниже:
record Customer(int age, String fullName) {
public Customer {
if (age < 0) {
throw new IllegalArgumentException("Величина, отражающая возраст клиента, должна быть положительным числом");
}
}
}
Как видно из этого примера, с помощью компактной записи конструктора, можно выполнять валидацию полей и выбрасывать исключения. Однако стоит отметить, что допускаются только unchecked исключения, проверяемые или checked исключения выбросить (throw) не получится.
В дополнение к каноническому конструктору можно создавать свои собственные, однако они обязательно должны ссылаться на другие конструкторы с использованием ключевого слова this:
record Customer (int age, String fullName) {
public Customer(int age) {
this(age, “Agent Smith”);
}
public Customer() {
this(100);
}
}
Очевидное применение таких конструкторов – задание значений по-умолчанию.
hashCode, equals и toString
Хотя Reсord обеспечивает автоматическое создание и поддержание этих методов в актуальном состоянии, их все же можно переопределить и обеспечить пользовательское поведение, как в примере ниже:
record Customer(int age, String fullName) {
@Override
public String toString() {
return "Переопределенное поведение метода toString";
}
}
Неизменяемость
Как уже упоминалось ранее, все поля в Record являются конечными (final), т.е. их изменение не возможно.
Однако используя компактную запись конструктора, мы можем их переопределить на этапе создания:
record Customer (int age, String fullName) {
public Customer {
fullName = “Modified Name”;
}
}
Использование ключевого слова this не допустимо. Следующий код приведет к ошибке компиляции:
record Customer (int age, String fullName) {
public Customer {
this.fullName = “Modified Name”;
}
}
И так, поскольку Record является неизменяемым типом, все объекты этого типа, после того как они были созданы, не могут быть изменены.
У Record не существует сеттеров, поскольку все поля являются конечными и сеттеры не могут быть созданы.
Единственный способ изменить какие-либо данные - создать новый объект:
Customer c1 = new Customer(35, “Nice guy”);
Customer c2 = new Customer(c1.age(), “Friend of that nice guy”);
Расширение функционала с помощью пользовательских методов и полей
Тип Record поддерживает добавление функционала путем написания собственных методов.
Частный случай, - валидация полей с использованием компактной записи конструктора, уже был рассмотрен ранее в статье.
И так, пример:
record Customer(int age, String fullName) {
public boolean isAdult() {
return age >= 18;
}
}
Пользовательские поля можно использовать внутри Record, только определив их в списке полей заголовка:
sealed class Gender permits Male, Female {}
final class Male extends Gender {}
final class Female extends Gender {}
record Customer(int age, String fullName, Gender gender) {}
Поля вне определения записи не разрешены, и следующий код работать не будет:
sealed class Gender permits Male, Female {}
final class Male extends Gender {}
final class Female extends Gender {}
record Customer(int age, String fullName) {
private final Gender gender;
}
Видимость полей
Особенность Record для каждого поля создавать публичный геттер является в некоторой степени недостатком, так как делает применение этого типа сфокусированным на данных, а не на поведении.
У вас нет возможности сделать поле private, и следующий код не будет компилироваться:
record Customer(private int age, String fullName) {}
Сериализация
Объекты типа Record хорошо подходят для сериализации и десериализации:
record Customer(int age, String fullName) impelements Serializable {}
Однако здесь важно отметить, что такие объекты сериализуются иначе, чем обычные сериализуемые или экстернализуемые объекты.
Сериализация объектов Record не может быть настроена, и любые специфические для класса (class-specific) методы writeObject, readObject, readObjectNoData, writeExternal и readExternal будут игнорироваться при сериализации и десериализации.
Статические методы и поля
Как и любой другой класс в Java, Record допускает использование статических методов и полей:
record Customer(int age, String fullName) {
private static final int adultAge = 18;
public boolean isAdult() {
return age >= adultAge;
}
public static Customer createAdultCustomer(String fullName){
return new Customer(adultAge, fullName);
}
}
Наследование
Для того, чтобы понять может ли быть унаследован Record или может ли он наследовать другие классы, рассмотрим его реализацию «под капотом»:
record Customer (int age, String fullName) {}
Если выполнить команду javap Customer, получим что-то подобное:
final class Customer extends java.lang.Record {
Customer(int, String);
public final java.lang.String toString();
public final int hashCode();
public final boolean equals(java.lang.Object);
public int age();
public String fullName();
}
Видим, что класс Customer являются final, поэтому его нельзя унаследовать.
В свою очередь он унаследован от класса java.lang.Record, а поскольку Java не допускает множественного наследования, получается что тип Record не может наследовать никакие другие классы, в том числе абстрактные.
Однако, разрешается использовать интерфейсы:
record Customer(int age, String fullName) implements Comparable<Customer>, Serializable {
@Override
public int compareTo(Customer c) {
return this.age - c.age;
}
}
Конец статьи
======================================================
Вдохновением для этой статьи стала статья «An In-Depth Guide to Java Records» автора Konrad Tendera.
Выражаю этому автору большую благодарность.