Что такое record в java
Начиная с версии Java 16 в язык была добавлена новая функциональность — Records (на русском нередко называют «записями»). Records представляют классы, которые предназначены для создания контейнеров неизменяемых данных. Кроме того, records позволяют упростить разработку, сократив объем кода.
Для определения классов record применяется ключевое слово record , после которого идет название и далее в круглых скобках список полей record:
record название (поле1, поле2. полеN) < // тело record >
Рассмотрим следующий пример
import java.util.Objects; public class Program < public static void main (String args[])< Person tom = new Person("Tom", 36); System.out.println(tom.toString()); >> class Person < private final String name; private final int age; Person(String name, int age) < this.name = name; this.age = age; >String name() < return name; >int age() < return age; >public boolean equals(Object o) < if (!(o instanceof Person)) return false; Person other = (Person) o; return other.name == name && other.age == age; >public int hashCode() < return Objects.hash(name, age); >public String toString() < return String.format("Person[name=%s, age=%d]", name, age); >>
Здесь определен класс Person, который определяет две константы — name и age:
private final String name; private final int age;
Их значения устанавливаются в конструкторе. Больше никак их установить мы не можем. Таким образом, после создания объекта Person они будут хранить неизменяемые данные.
Для получения значений name и age предусмотрены одноименные методы:
String name() < return name; >int age()
Кроме того, здесь переопределены унаследованные от класса Object методы equals() , hashCode() и toString() .
В методе main создаем один объект класса Person и выводит на консоль его текстовое представление:
Person tom = new Person("Tom", 36); System.out.println(tom.toString());
В итоге консоль нам выведет:
Person[name=Tom, age=36]
Теперь посмотрим, что нам предлагают Records — определим record, которая будет полностью аналогична вышеопределенному классу:
public class Program < public static void main (String args[])< Person tom = new Person("Tom", 36); System.out.println(tom.toString()); >> record Person(String name, int age)
Records определяются с помощью ключевого слова record , за которым следует название записи. Дальше идет список полей записи. То есть в данном случае определяется два поля — name и age. Причем по умолчанию все они будут приватными и иметь модификатор final .
Также будет создаваться конструктор с двумя параметрами name и age. А каждого поля автоматически будет создаваться одноименный общедоступный метод для получения значения это поля. Например, для поля name создается метод name() , который возвращает значение поля name.
И также автоматически будут создаваться методы equals, hashCode и toString . Вообщем, данная record будет полностью аналогична вышеопределенному классу, но при этом содержит гораздо меньше кода.
При необходимости мы можем вызывать все имеющиеся методы:
public class Program < public static void main (String args[])< Person tom = new Person("Tom", 36); System.out.println(tom.name()); // Tom System.out.println(tom.age()); // 36 System.out.println(tom.hashCode()); Person bob = new Person("Bob", 21); Person tomas = new Person("Tom", 36); System.out.println(tom.equals(bob)); // false System.out.println(tom.equals(tomas)); // true >> record Person(String name, int age)
Конструктор record
В примере выше применялась форма record:
record Person(String name, int age)
которая фактически создавала конструктор:
Person(String name, int age)
Этот конструктор называется каноническим . Он принимает параметры, которые называются также как и поля record, и передает полям значения соответствующих параметров.
Тем не менее при необходимости мы можем изменить логику конструктора. Так, мы можем переопределить логику конструктора. Например, что если при создании объекта будет передан невалидный возраст? Предусмотрим эту ситуацию, переопределив логику конструктора:
public class Program < public static void main (String args[])< Person tom = new Person("Tom", -116); System.out.println(tom.toString()); >> record Person(String name, int age) < Person< if(age110) < age = 18; >> >
В данном случае если передано невалидное значение, то применяем некоторое значение по умолчанию (число 18). В итоге фактически мы получим конструктор со следующим действием:
Person(String name, int age) < if(age110) < age = 18; >this.name = name; this.age = age; >
В итоге программа выведет на консоль следующее:
Person[name=Tom, age=18]
Мы можем полностью переопределить канонический конструктор:
public class Program < public static void main (String args[])< Person tom = new Person("Tom", 36); System.out.println(tom.toString()); System.out.println(tom.name()); >> record Person(String name, int age) < Person(String name, int age)< if(age < 0 || age >120) age = 18; this.name = name; this.age = age; > >
Также мы можем определять какие-то другие конструкторы, но все они должны вызывать канонический конструктор:
public class Program < public static void main (String args[])< Person tom = new Person("Tom", "Smith", 36); System.out.println(tom.toString()); >> record Person(String name, int age) < Person(String firstName, String lastName, int age)< this(firstName + " " + lastName, age); >>
Здесь определен конструктор, который условно принимает имя, фамилию и возраст пользователя. Этот конструктор вызывает канонический конструктор, передая ему значения для полей name и age: this(firstName + » » + lastName, age) .
Консольный вывод программы:
Person[name=Tom Smith, age=36]
Переопределение методов
Также мы можем переопределить методы, которые имеет record по умолчанию. А это методы equals() , hashCode() и toString() и методы, которые называются также, как и поля записи, и которые возвращают значения соответствующих полей.
Например, перепопределим для записи Person методы toString() и name() :
public class Program < public static void main (String args[])< Person tom = new Person("Tom", 36); System.out.println(tom.toString()); System.out.println(tom.name()); >> record Person(String name, int age) < public String name() < return "Mister " + name; >public String toString() < return String.format("Person %s, Age: %d", name, age); >>
Консольный вывод программ:
Person Tom, Age: 36 Mister Tom
Ограничения records
Следует учитывать, что мы не можем наследовать запись record от других классов. Также нельзя наследовать классы от records. Однако классы record могут реализовать интерфейсы. Кроме того, классы record не могут быть абстрактными.
В record нельзя явным образом определять нестатические поля и инициализаторы. Но можно определять статические переменные и инициализаторы, также как статические и нестатические методы:
record Person(String name, int age) < static int minAge; static< minAge = 18; System.out.println("Static initializer"); >>
Что такое record в java
JDK 14 introduces records, which are a new kind of type declaration. Like an enum , a record is a restricted form of a class. It’s ideal for «plain data carriers,» classes that contain data not meant to be altered and only the most fundamental methods such as constructors and accessors.
This is a preview feature, which is a feature whose design, specification, and implementation are complete, but is not permanent, which means that the feature may exist in a different form or not at all in future JDK releases. To compile and run code that contains preview features, you must specify additional command-line options. See Preview Features.
For background information about records, see JEP 359.
Consider the following class definition:
final class Rectangle implements Shape < final double length; final double width; public Rectangle(double length, double width) < this.length = length; this.width = width; >double length() < return length; >double width() < return width; >>
It has the following characteristics:
- All of its members are declared final
- Its only methods consist of a constructor, Rectangle(double length, double width) and two accessors, length() and width()
You can represent this class with a record:
record Rectangle(float length, float width)
A record consists of a name (in this example, it’s Rectangle ) and a list of the record’s components (which in this example are float length and float width ).
A record acquires these members automatically:
- A private final field for each of its components
- A public read accessor method for each component with the same name and type of the component; in this example, these methods are Rectangle::length() and Rectangle::width()
- A public constructor whose signature is derived from the record components list. The constructor initializes each private field from the corresponding argument.
- Implementations of the equals() and hashCode() methods, which specify that two records are equal if they are of the same type and their corresponding record components are equal
- An implementation of the toString() method that includes the string representation of all the record’s components, with their names
If you want your record’s constructor to do more than initialize its private fields, you can define a custom constructor for the record. However, unlike a class constructor, a record constructor doesn’t have a formal parameter list; this is called a compact constructor .
For example, the following record, HelloWorld , has one field, message . Its custom constructor calls Objects.requireNonNull(message) , which specifies that if the message field is initialized with a null value, then a NullPointerException is thrown. (Custom record constructors still initialize their record’s private fields.)
record HelloWorld(String message) < public HelloWorld < java.util.Objects.requireNonNull(message); >>
Restrictions on Records
The following are restrictions on the use of records:
- Records cannot extend any class
- Records cannot declare instance fields (other than the private final fields that correspond to the components of the record component list); any other declared fields must be static
- Records cannot be abstract; they are implicitly final
- The components of a record are implicitly final
Beyond these restrictions, records behave like regular classes:
- You can declare them inside a class; nested records are implicitly static
- You can create generic records
- Records can implement interfaces
- You instantiate records with the new keyword
- You can declare in a record’s body static methods, static fields, static initializers, constructors, instance methods, and nested types
- You can annotate records and a record’s individual components
APIs Related to Records
The class java.lang.Class has two new methods related to records:
- RecordComponent[] getRecordComponents() : Returns an array of java.lang.reflect.RecordComponent objects, which correspond to the record’s components.
- boolean isRecord() : Similar to isEnum() except that it returns true if the class was declared as a record.
Кофе-брейк #230. Что такое Записи (Records) в Java и как они работают
Источник: JavaTechOnline В этой статье подробно рассмотрена концепция работы записей (Records) в Java с примерами, включая их синтаксис, способы создания и использования. Записи (Records) в Java — одна из замечательных функций, впервые показанная в Java 14 в качестве функции предварительного просмотра, и окончательно появившаяся в релизе Java 17. Многие разработчики активно ее используют, что помогает им успешно сокращать огромное количество шаблонного кода. Более того: благодаря записям не нужно писать ни одной строки кода, чтобы сделать класс неизменяемым.
Когда использовать Record в Java?
Если вы хотите передавать неизменяемые данные между разными уровнями вашего приложения, то использование Record может стать хорошим выбором. По умолчанию записи (Records) в Java являются неизменяемыми, а это означает, что мы не можем изменить их свойства после их создания. В результате это помогает нам избежать ошибок и повышает надежность кода. Проще говоря, используя Record в Java, мы можем автоматически генерировать классы.
Где можно использовать Record в Java?
Как правило, мы можем использовать записи в любой ситуации, когда нужно объявить простые контейнеры данных с неизменяемыми свойствами и автоматически сгенерированными методами. Например, ниже приведены несколько вариантов использования, в которых записи могут быть полезны: Объекты передачи данных (Data transfer objects, DTO): мы можем использовать Record для объявления простых объектов передачи данных, которые содержат данные. Это полезно при передаче данных между разными уровнями приложения, например, между уровнем службы и уровнем базы данных. Объекты конфигурации : Record можно использовать для объявления объектов конфигурации, которые содержат набор свойств конфигурации для приложения или модуля. Эти объекты обычно имеют неизменяемые свойства, что делает их потокобезопасными и простыми в использовании. Объекты-значения. Record можно использовать для объявления объектов-значений, которые содержат набор значений, представляющих определенную концепцию или модель предметной области. API Responses (ответы API) : при создании REST API данные обычно возвращаются в форме JSON или XML. В таких случаях может потребоваться определить простую структуру данных, представляющую ответ API. Записи идеально подходят для этого, потому что они позволяют определить легкую и неизменяемую структуру данных, которую можно легко сериализовать в JSON или XML. Тестовые данные. При написании модульных тестов часто необходимо создавать тестовые данные, представляющие определенный сценарий. В таких случаях может потребоваться определить простую структуру данных, представляющую тестовые данные. Records могут быть идеальными для этого, потому что они позволяют нам определить легкую и неизменяемую структуру данных с минимальным шаблонным кодом. Tuple-подобные объекты : записи могут использоваться для объявления Tuple-подобных объектов, которые содержат фиксированное количество связанных значений. Это может быть полезно при возврате нескольких значений из метода или при работе с коллекциями связанных значений.
Что такое запись (Record) в Java?
Запись (Record) в Java — это класс, предназначенный для хранения данных. Он похож на традиционный класс Java, но более легкий и обладает некоторыми уникальными функциями. Записи неизменяемы по умолчанию, что означает, что их состояние не может быть изменено после их создания. Это делает их идеальными для хранения неизменяемых данных, таких как параметры конфигурации или значения, возвращенные из запроса к базе данных. Также это способ создания пользовательского типа данных, состоящего из набора полей или переменных, а также методов доступа к этим полям и их изменения. Record в Java упрощает работу с данными за счет уменьшения объема шаблонного кода, который разработчики используют для написания снова и снова. Синтаксис создания записи в Java:
record Record_Name(Fields. )
Как создать и использовать Record в Java с примерами?
Давайте разберемся, как создать и использовать запись в Java программными методами.
Создание Record
Программно создание записи не очень похоже на создание обычного класса в Java. Вместо class мы используем ключевое слово record . Также нужно объявить поля с типами данных в скобках имени записи. Вот пример кода, который демонстрирует, как создать запись в Java:
public record Book(String name, double price)
В этом примере мы создали запись под названием Book с двумя полями: name и price . Ключевое слово public указывает, что к этой записи можно получить доступ вне пределов пакета, в котором она определена.
Использование Record
Чтобы использовать запись в Java, мы можем создать ее экземпляр, применяя ключевое слово new, как мы это делаем с обычным классом. Вот пример:
Book book = new Book( "Core Java" , 324.25);
Здесь создается новый экземпляр записи Book с полем name , установленным на Core Java , и полем price , установленным на 324,25 . После создания записи к ее полям можно получить доступ, используя имя самого поля. Методов получения и установки нет. Вместо этого имя поля становится именем метода.
String name = book.name(); double price = book.price();
Здесь мы видим извлечение значения полей name и price из записи Book . В дополнение к полям записи также имеют некоторые встроенные методы, которые можно использовать для управления ими. Например, toString() , equals() и hashCode() . Помните, что записи в Java по умолчанию являются неизменяемыми, что означает, что после их создания их состояние нельзя изменить. Давайте рассмотрим еще один пример того, как создавать и использовать записи в Java. Возьмем случай, где нам нужна запись для хранения информации о студенте.
public record Student(String name, int age, String subject) <>
Здесь создается запись под названием Student с тремя полями: name , age и subject . Создать экземпляр этой записи мы можем следующим образом:
Student student = new Student("John Smith", 20, "Computer Science");
Затем мы можем получить значения полей, используя имя поля:
String name = student.name(); int age = student.age(); String subject= student.subject();
Как выглядит запись (Record) после компиляции?
Так как Record — это просто особый вид класса, компилятор также преобразует его в обычный класс, но с некоторыми ограничениями и отличиями. Когда компилятор преобразует запись (файл Java) в байт-код после процесса компиляции, созданный файл .class содержит некоторые дополнительные объявления класса Record . Например, ниже приведен байт-код, созданный для записи Student компилятором Java:
record Student(String name, int age)
Запись в файл .class (после компиляции)
- Компилятор заменил ключевое слово Record на class .
- Компилятор объявил класс как final . Это указывает на то, что этот класс не может быть расширен. Это также означает, что он не может быть унаследован и неизменен по своей природе. Преобразованный класс расширяет java.lang.Record . Это указывает на то, что все записи являются подклассом класса Record , определенного в пакете java.lang .
- Компилятор добавляет параметризованный конструктор.
- Компилятор автоматически сгенерировал методы toString() , hashCode() и equals() .
- Компилятор добавил методы для доступа к полям. Обратите внимание на соглашение об именах методов — они точно совпадают с именами полей, перед именами полей не должно быть get или set .
Поля в Records
Записи в Java определяют свое состояние с помощью набора полей, каждое из которых имеет свое имя и тип. Поля записи объявляются в заголовке записи. Например:
public record Person(String name, int age) <>
Эта запись имеет два поля: name типа String и age типа int . Поля записи неявно являются final и не могут быть переназначены после создания записи. Мы можем добавить новое поле, но это не рекомендуется. Новое поле, добавляемое в запись, должно быть статическим. Например:
public record Person(String name, int age)
Конструкторы в Records
Как и традиционные конструкторы классов, конструкторы в записях используются для создания экземпляров записей. В Records есть две концепции конструкторов: канонический конструктор и компактный конструктор. Компактный конструктор обеспечивает более краткий способ инициализации переменных состояния в записи, тогда как канонический конструктор предоставляет более традиционный способ с большей гибкостью.
Канонический конструктор
Компилятор Java по умолчанию предоставляет нам конструктор со всеми аргументами (конструктор со всеми полями), который присваивает свои аргументы соответствующим полям. Он известен как канонический конструктор. Мы также можем добавить бизнес-логику, такую как условные операторы, для проверки данных. Ниже показан пример:
public record Person(String name, int age) < public Person(String name, int age) < if (age < 18) < throw new IllegalArgumentException("You are not allowed to participate in general elections"); >> >
Компактный конструктор
Компактные конструкторы игнорируют все аргументы, включая круглые скобки. Назначение соответствующих полей происходит автоматически. Например, следующий код демонстрирует концепцию компактного конструктора в Records :
public record Person(String name, int age) < public Person < if (age < 18) < throw new IllegalArgumentException("You are not allowed to participate in general elections"); >> >
В дополнение к компактному конструктору вы можете определить обычные конструкторы в Record , как и в обычном классе. Однако нужно убедиться, что все конструкторы инициализируют все поля записи.
Методы в Records
Записи в Java автоматически генерируют набор методов для каждого поля в записи. Эти методы называются методами доступа и имеют то же имя, что и поле, с которым они связаны. Например, если в записи есть поле с именем price , то она автоматически будет иметь метод с именем price() , который возвращает значение поля price . В дополнение к автоматически сгенерированным методам доступа мы также можем определить наши собственные методы в записи, как и в обычном классе. Например:
public record Person(String name, int age) < public void sayHello() < System.out.println("Hello, my name is " + name); >>
В этой записи есть метод sayHello() , который выводит приветствие, используя поле name .
Как Record помогает сократить шаблонный код
Давайте рассмотрим простой пример, чтобы увидеть, как записи в Java могут помочь избавиться от шаблонного кода. Предположим, у нас есть класс с именем Person , который представляет человека с именем, возрастом и адресом электронной почты. Вот как мы определяем класс традиционным способом. Код без использования Record :
public class Person < private String name; private int age; private String email; public Person(String name, int age, String email) < this.name = name; this.age = age; this.email = email; >public String getName() < return name; >public int getAge() < return age; >publicString getEmail() < return email; >public void setName(String name) < this.name = name; >public voidsetAge(int age) < this.age = age; >public void setEmail(String email) < this.email = email; >@Override public String toString() < return "Person'; > >
- Объявить класс как final , чтобы его нельзя было расширить.
- Объявить все поля private и final , чтобы их нельзя было изменить вне конструктора.
- Не предоставлять никаких методов setter для полей.
- Если какое-либо из полей является изменяемым, нужно вернуть их копию вместо возврата исходного объекта.
public record Person(String name, int age, String email) <>
Вот и все! Всего одной строкой кода мы определили класс, который имеет те же поля, конструктор, геттеры, сеттеры и метод toString() , что и традиционный класс. Синтаксис Record позаботится обо всем шаблонном коде за нас. Из приведенного выше примера ясно, что использование записей в Java может помочь избавиться от шаблонного кода, предоставляя более краткий синтаксис для определения классов с фиксированным набором полей. По сравнению с традиционным подходом для определения и использования записей требуется меньше кода, и записи обеспечивают прямой доступ к полям с помощью методов для обновления полей. Это делает код более удобным для чтения, ремонтопригодным и менее подверженным ошибкам. Кроме того, с синтаксисом Record нам не нужно делать ничего дополнительно, чтобы сделать класс неизменяемым. По умолчанию все поля в записи являются final , а сам класс записи неизменяем. Следовательно, создание неизменяемого класса с использованием синтаксиса записи намного проще и требует меньше кода, чем традиционный подход. С записями нам не нужно объявлять поля как final , предоставлять конструктор, который инициализирует поля, или предоставлять геттеры для всех полей.
Общие классы записей
В Java также можно определить общие классы Records . Универсальный класс записи — это класс записи, который имеет один или несколько параметров типа. Перед вами пример:
public record Pair(T first, U second) <>
В этом примере Pair — это универсальный класс записи, который принимает два параметра типа T и U . Экземпляр этой записи мы можем создать следующим образом:
Pairpair = new Pair<>( "Hello" , 123);
Вложенный класс внутри Record
Внутри записи также можно определить вложенные классы и интерфейсы. Это полезно для группировки связанных классов и интерфейсов, также это может помочь улучшить организацию и удобство сопровождения кодовой базы. Вот пример записи, содержащей вложенный класс:
public record Person(String name, int age, Contact contact) < public static class Contact < private final String email; private final String phone; public Contact(String email, String phone)< this.email = email; this.phone = phone; >public String getEmail() < return email; >public String getPhone() < return phone; >> >
В этом примере Person — это запись, содержащая вложенный класс Contact . В свою очередь, Contact — это статический вложенный класс, который содержит два приватных конечных поля: адрес электронной почты и телефон. Он также имеет конструктор, принимающий электронную почту и номер телефона, и два метода получения: getEmail() и getPhone() . Экземпляр Person мы можем создать следующим образом:
Person person = new Person("John",30, new Person.Contact("john@example.com", "123-456-7890"));
В этом примере мы создали новый объект Person с именем John , возрастом 30 лет и новый объект Contact с электронной почтой john@example.com и телефоном 123-456-7890 .
Вложенный интерфейс внутри Record
Вот пример записи, содержащей вложенный интерфейс:
public record Book(String title, String author, Edition edition) < public interface Edition< String getName(); >>
В данном примере Book — это запись, содержащая вложенный интерфейс Edition . В свою очередь, Edition — это интерфейс, определяющий единственный метод getName() . Экземпляр Book мы можем создать следующим образом:
Book book = new Book("The Hitchhiker's Guide to the Galaxy", "Douglas Adams", new Book.Edition() < public String getName() < return "Science Fiction"; >>);
В этом примере мы создаем новый объект Book с заголовком The Hitchhiker’s Guide to the Galaxy , автором Douglas Adams и новой анонимной реализацией интерфейса Edition , которая возвращает имя Science Fiction , когда вызывается метод getName() .
Что еще могут делать Records?
- Записи могут определять пользовательские конструкторы. Записи поддерживают параметризованные конструкторы, которые могут вызывать конструктор по умолчанию с предоставленными параметрами в своих телах. Кроме того, записи также поддерживают компактные конструкторы, которые аналогичны конструкторам по умолчанию, но могут включать дополнительные функции, такие как проверки в теле конструктора.
- Как и любой другой класс в Java, Record может определять и использовать методы экземпляра. Это означает, что мы можем создавать и вызывать методы, специфичные для класса записи.
- В Record определение переменных экземпляра как членов класса не допускается, поскольку они могут быть указаны только как параметры конструктора. Однако записи поддерживают статические поля и статические методы, которые можно использовать для хранения и доступа к данным, общим для всех экземпляров класса записи.
Может ли запись реализовывать интерфейсы?
Да, запись в Java может реализовывать интерфейсы. Например, приведенный ниже код демонстрирует концепцию:
public interface Printable
public record Person(String name, int age) implements Printable < public void print() < System.out.println("Name: " + name + ", Age: " + age); >>
Здесь мы определили интерфейс Printable с единственным методом print() . Мы также определили запись Person , которая реализует интерфейс Printable . Запись Person имеет два поля: name и age , и переопределяет метод печати интерфейса Printable для печати значений этих полей. Мы можем создать экземпляр записи Person и вызвать его метод print следующим образом:
Person person = new Person("John", 30); person.print();
Это выведет на консоль Name: John, Age: 30 . Как показано в примере, записи в Java могут реализовывать интерфейсы точно так же, как и обычные классы. Это может быть полезно для добавления поведения к записи или для обеспечения соответствия записи контракту, определенному интерфейсом.
Кофе-брейк #128. Руководство по использованию Java Records
Источник: abhinavpandey.dev В этом руководстве мы рассмотрим основы использования записей (Records) в Java. Записи появились в Java 14 как способ удалить шаблонный код вокруг создания объектов-значений (Value objects), используя преимущества неизменяемых объектов.
1. Основные понятия
Прежде чем перейти непосредственно к записям, давайте рассмотрим проблему, которую они решают. Для этого нам придется вспомнить, как объекты-значения создавались до Java 14.
1.1. Объекты-значения (Value objects)
Объекты-значения (Value objects) являются неотъемлемой частью приложений Java. В них хранятся данные, которые необходимо передавать между уровнями приложения. Объект-значение содержит поля, конструкторы и методы для доступа к этим полям. Ниже приведен пример объекта-значения:
public class Contact < private final String name; private final String email; public Contact(String name, String email) < this.name = name; this.email = email; >public String getName() < return name; >public String getEmail() < return email; >>
1.2. Равенство между Value objects
Объекты-значения также могут предоставлять способ их сравнения на предмет равенства. По умолчанию Java сравнивает равенство объектов путем сравнения их адреса памяти. Однако в некоторых случаях объекты, содержащие одинаковые данные, могут считаться равными. Чтобы реализовать это, мы можем переопределить методы equals и .hashCode . Давайте реализуем их для класса Contact :
public class Contact < // . @Override public boolean equals(Object o) < if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Contact contact = (Contact) o; return Object.equals(email, contact.email) && Objects.equals(name, contact.name); >@Override public int hashCode() < return Objects.hash(name, email); >>
1.3. Неизменяемость Value objects
- Чтобы избежать риска случайного изменения значения поля.
- Чтобы убедиться, что равные объекты остаются одинаковыми на протяжении всей их жизни.
- сделали поля private и final .
- предоставили только getter для каждого поля (без setters ).
1.4. Регистрация объектов Value objects
Часто нам требуется регистрировать значения, содержащиеся в объектах. Это делается путем предоставления метода toString . Всякий раз, когда объект регистрируется или печатается, вызывается метод toString . Здесь самый простой способ — распечатать значение каждого поля. Вот пример:
public class Contact < // . @Override public String toString() < return "Contact[" + "name='" + name + '\'' + ", email code">Contact, которая имеет ту же функциональность, что и класс Contact, определенный выше. public record Contact(String name, String email) <>
Ключевое слово record используется для создания класса Record . Записи могут обрабатываться вызывающей стороной точно так же, как класс. Например, чтобы создать новый экземпляр записи, мы можем использовать ключевое слово new .
Contact contact = new Contact("John Doe", "johnrocks@gmail.com");
2.2. Поведение по умолчанию
- Поля name и email являются private и final по умолчанию.
- Код определяет “канонический конструктор”, который принимает поля в качестве параметров.
- Поля доступны через геттер-подобные методы — name() и email() . Для полей нет установщика, поэтому данные в объекте становятся неизменяемыми.
- Реализован метод toString для печати полей так же, как мы делали это для класса Contact .
- Реализованы методы equals и .hashCode . Они включают все поля, как и класс Contact .
2.3 Канонический конструктор
Конструктор, определенный по умолчанию, принимает все поля в качестве входных параметров и устанавливает их в поля. Например, ниже показан канонический конструктор (Canonical Constructor), определенный по умолчанию:
public Contact(String name, String email)
Если мы определим конструктор с такой же сигнатурой в классе записи, он будет использоваться вместо канонического конструктора.
3. Работа с записями
Мы можем изменить поведение записи несколькими способами. Давайте рассмотрим некоторые варианты использования и способы их достижения.
3.1. Переопределение реализаций по умолчанию
Любую реализацию по умолчанию можно изменить, переопределив ее. Например, если мы хотим изменить поведение метода toString , то мы можем переопределить его между фигурными скобками <> .
public record Contact(String name, String email) < @Override public String toString() < return "Contact[" + "name is '" + name + '\'' + ", email is" + email + ']'; >>
Точно так же мы можем переопределить методы equals и hashCode .
3.2. Компактные конструкторы
Иногда мы хотим, чтобы конструкторы делали больше, чем просто инициализировали поля. Для этого мы можем добавить необходимые операции в нашу запись в компактном конструкторе (Compact Constructor). Он называется компактным, потому что ему не нужно определять инициализацию полей или список параметров.
public record Contact(String name, String email) < public Contact < if(!email.contains("@")) < throw new IllegalArgumentException("Invalid email"); >> >
Обратите внимание, что список параметров отсутствует, а инициализация name и email происходит в фоновом режиме перед выполнением проверки.
3.3. Добавление конструкторов
В запись можно добавить несколько конструкторов. Давайте рассмотрим пару примеров и ограничений. Для начала добавим новые допустимые конструкторы:
public record Contact(String name, String email) < public Contact(String email) < this("John Doe", email); >// replaces the default constructor public Contact(String name, String email) < this.name = name; this.email = email; >>
В первом случае доступ к конструктору по умолчанию осуществляется с помощью ключевого слова this . Второй конструктор переопределяет конструктор по умолчанию, поскольку он имеет тот же список параметров. В этом случае запись сама по себе не создаст конструктор по умолчанию. Существует несколько ограничений на конструкторы.
1. Конструктор по умолчанию всегда должен вызываться из любого другого конструктора.
Например, приведенный ниже код не будет компилироваться:
public record Contact(String name, String email) < public Contact(String name) < this.name = "John Doe"; this.email = null; >>
Это правило гарантирует, что поля всегда инициализируются. Также гарантируется, что операции, определенные в компактном конструкторе, всегда выполняются.
2. Невозможно переопределить конструктор по умолчанию, если определен компактный конструктор.
Когда компактный конструктор определен, автоматически создается конструктор по умолчанию с логикой инициализации и компактного конструктора. В этом случае компилятор не позволит нам определить конструктор с теми же аргументами, что и конструктор по умолчанию. Например, в этом коде компиляция не произойдет:
public record Contact(String name, String email) < public Contact < if(!email.contains("@")) < throw new IllegalArgumentException("Invalid email"); >> public Contact(String name, String email) < this.name = name; this.email = email; >>
3.4. Реализация интерфейсов
Как и в любом классе, в записях мы можем реализовать интерфейсы.
public record Contact(String name, String email) implements Comparable < @Override public int compareTo(Contact o) < return name.compareTo(o.name); >>
Важное примечание. Для обеспечения полной неизменности записи не могут участвовать в наследовании. Записи являются окончательными (final) и не могут быть расширены. Они также не могут расширять другие классы.
3.5. Добавление методов
В дополнение к конструкторам, которые переопределяют методы и реализации интерфейсов, мы также можем добавлять любые методы по своему желанию. Например:
public record Contact(String name, String email) < String printName() < return "My name is:" + this.name; >>
Также мы можем добавить статические методы. Например, если мы хотим иметь статический метод, который возвращает регулярное выражение, по которому можно проверять электронную почту, то мы можем определить его, как показано ниже:
public record Contact(String name, String email) < static Pattern emailRegex() < return Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]$", Pattern.CASE_INSENSITIVE); > >
3.6. Добавление полей
Мы не можем добавить поля экземпляра в запись. Однако мы можем добавить статические поля.
public record Contact(String name, String email) < private static final Pattern EMAIL_REGEX_PATTERN = Pattern .compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]$", Pattern.CASE_INSENSITIVE); static Pattern emailRegex() < return EMAIL_REGEX_PATTERN; >>
Обратите внимание, что в статических полях нет неявных ограничений. При необходимости они могут быть общедоступными и не окончательными.
Заключение
Записи — отличный способ определить классы данных. Они намного удобнее и мощнее, чем подход JavaBeans/POJO. Из-за простоты реализации им следует отдавать предпочтение перед другими способами создания объектов-значений.