Записи Java — это функция, представленная в качестве функции предварительного просмотра в Java 14 и доработанная позже, чтобы обеспечить краткий и удобный способ объявления классов, которые в основном используются для хранения данных. Они предназначены для упрощения создания неизменяемых классов за счет автоматической генерации необходимого кода для инкапсуляции полей данных, конструкторов, методов доступа и других распространенных методов.
Основные мотивы внедрения Java Records:
Краткость. Записи сокращают шаблонный код, автоматически создавая стандартные реализации для общих задач, таких как средства доступа к полям, методы
equals()
,hashCode()
иtoString()
. Это позволяет разработчикам сосредоточиться на определении полей данных и их поведении.
Неизменяемость. По умолчанию записи неизменяемы. Это означает, что после создания объекта его состояние нельзя изменить. Это свойство желательно во многих случаях, когда важны целостность и согласованность данных.
Удобочитаемость. Благодаря использованию записей назначение кода становится более ясным. Записи явно сообщают, что класс в первую очередь является контейнером данных с определенным набором полей.
Сопоставление шаблонов. Записи Java хорошо работают с функцией сопоставления шаблонов, представленной в Java 14. Их можно использовать в качестве шаблонов для упрощения кода, включающего извлечение и сравнение данных.
Как создавать записи?
Создадим неизменяемый класс:
public final class Account { private final String name; private final int id; private final String type; public Account(String name, int id, String type) { this.name = name; this.id = id; this.type = type; } public String name() { return name; } public int id() { return id; } public String type() { return type; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Account that = (Account) o; return id == that.id && Objects.equals(name, that.name) && Objects.equals(type, that.type); } @Override public int hashCode() { return Objects.hash(name, id, type); } @Override public String toString() { return "Account{" + "name='" + name + '\'' + ", id=" + id + ", type='" + type + '\'' + '}'; } }
Теперь давайте создадим такой же неизменяемый класс, используя запись. Нам нужно использовать ключевое слово record
для создания класса записи в Java. Как и в конструкторах, нам нужно упомянуть атрибуты и их типы в записи. В данном примере Account является типом record и используется для хранения информации об учетной записи:
public record Account(String name, int id, String type) {}
Как мы видим, мы можем сократить полный класс с помощью одной строки кода. Мы заменили ключевое слово class
на ключевое слово record
и позволили волшебству простоты произойти.
Ключевое слово record
в Java автоматически генерирует для нас несколько методов, включая конструктор, геттеры, equals()
, hashCode()
и toString()
. Эти методы используются для удобного создания и управления объектами класса записи. Например, после определения записи Account
мы можем создать экземпляр класса Account
следующим образом:
Account account = new Account("John Doe", 123456, "Savings");
При этом создается новый объект Account
с указанными значениями полей name
, id
и type
. Поскольку это запись, мы можем напрямую обращаться к полям, используя сгенерированные методы получения. Например:
String accountName = account.name(); int accountId = account.id(); String accountType = account.type(); System.out.println(accountName); System.out.println(accountId); System.out.println(accountType); // Output John Doe 123456 Savings
Важные моменты:
- Мы не можем расширить класс Record явно
Хотя все записи расширяют класс java.lang.Record
, мы не можем явно создать подкласс java.lang.Record
. Компилятор не пройдет. Это означает, что единственный способ получить запись — это явно объявить ее
final class Account extends Record { // Compiler error : The type Data may not subclass Record explicitly private final int id; }
2. Реализация интерфейсов
Классы записей позволяют нам реализовывать интерфейсы. Мы можем реализовать любой интерфейс, который захотим, будь то один интерфейс или несколько интерфейсов.
public record Account( int id, String name) implements Runnable, Serializable
3. Невозможно определить наши собственные переменные экземпляра
Когда мы определяем заголовок, он представляет состояние класса записи. Это означает, что у нас не может быть никакой другой переменной экземпляра внутри записи. Единственная переменная экземпляра, которая будет создана, — это та, которая указана в компоненте заголовка. Однако у нас могут быть статические переменные внутри записей, доступ к которым можно получить так же, как и к классам, используя имя класса записи.
4. Несколько конструкторов
Определение типа записи Java может содержать несколько конструкторов. Вот пример записи Java, который определяет дополнительный конструктор для типа записи Account:
public record Account( int id, String name) { public Account(int id) { this(id, null); } }
Дополнительный конструктор объявляется внутри тела объявления Account Record. Обратите внимание, как дополнительный конструктор вызывает конструктор по умолчанию записи учетной записи. Это требуется компилятору Java, поэтому компилятор знает, какие параметры конструктора в дополнительном конструкторе соответствуют параметрам конструктора по умолчанию.
Мы можем добавить столько дополнительных конструкторов, сколько имеет смысл для нашего конкретного определения записи Java.
5. Методы экземпляра
Мы можем добавить методы экземпляра в определение записи Java — так же, как мы можем сделать это с обычным классом Java. Вот пример определения записи Java учетной записи из предыдущих разделов с добавленным методом экземпляра с именем nameAsUpperCase()
:
public record Account( int id, String name) { public static String nameAsUpperCase() { return name().toUpperCase(); } }
Обратите внимание, как метод nameAsUperCase()
внутри вызывает автоматически сгенерированный метод name()
.
6. Статические методы
Также возможно добавить статические методы к определению записи Java. Вот пример:
public record Account( int id, String name) { public static String nameAsUpperCase(Account acc) { return acc.name.toUpperCase(); } }
7. Использование аннотаций
Мы можем добавлять аннотации к компонентам записи. Например, мы можем применить аннотацию @Transient
к полю id
.
public record Account(@Transient Long id,String name, String type) { // ... }
8. Сериализация и десериализация
Сериализация записей в Java отличается от обычных классов. Когда объект записи сериализуется, его сериализованная форма состоит из последовательности значений, полученных из полей конечного экземпляра объекта. Формат потока объекта записи остается таким же, как у обычного объекта в потоке.
Во время десериализации, если локальный класс, соответствующий предоставленному дескриптору класса потока, является классом записи, поля потока считываются и реконструируются для представления значений компонентов записи. Впоследствии объект записи создается путем вызова канонического конструктора записи со значениями компонентов в качестве аргументов. Если значение компонента отсутствует в потоке, используется значение по умолчанию для типа компонента.
По умолчанию serialVersionUID класса записи равен 0L, если он не объявлен явно. Требование соответствия значений serialVersionUID также не применяется для классов записей. Процесс сериализации объектов записи нельзя настроить. Любые специфичные для класса методы, такие как writeObject, readObject, readObjectNoData, readResolve, writeExternal и readExternal, определенные в классах записей, игнорируются во время сериализации и десериализации. Однако метод writeReplace можно использовать для возврата альтернативного объекта для сериализации.
Перед выполнением какой-либо сериализации или десериализации мы должны убедиться, что запись должна быть либо сериализуемой, либо экстернализируемой.
public record Account (...) implements Serializable { }
Заключение
Типы записей в Java — очень полезная функция и ценное дополнение к любой системе на основе Java. Внедряя записи, многие приложения могут повысить ясность и лаконичность своих доменных классов. Кроме того, группы могут устранить потребность в реализации базового шаблона вручную и потенциально уменьшить или исключить свою зависимость от таких библиотек, как Lombok.
Счастливого обучения !!