Источник: JavaTechOnline В этой статье подробно рассмотрена концепция работы записей (Records) в Java с примерами, включая их синтаксис, способы создания и использования. Кофе-брейк #230. Что такое Записи (Records) в Java и как они работают - 1Записи (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 (после компиляции)

Мы также можем найти приведенный ниже преобразованный класс, если воспользуемся инструментом javap и применим приведенную ниже команду из командной строки. Важно отметить, что командная строка Java включает инструмент javap, который можно использовать для просмотра сведений о полях, конструкторах и методах файла класса. >javap Student Вывод: если мы внимательно проверим байт-код, то у нас могут возникнуть некоторые наблюдения:
  • Компилятор заменил ключевое слово 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) {

   static String sex;
}

Конструкторы в 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{" +
         "name='" + name + '\'' +
         ", age=" + age +
         ", email='" + email + '\'' +
      '}';
    }
}
Как мы видим, этому классу требуется много шаблонного кода для определения полей, конструктора, геттеров, сеттеров и метода toString(). Кроме того, предположим, что мы хотим сделать этот класс неизменяемым. Для этого мы должны будем сделать некоторые дополнительные шаги, такие как:
  • Объявить класс как final, чтобы его нельзя было расширить.
  • Объявить все поля private и final, чтобы их нельзя было изменить вне конструктора.
  • Не предоставлять никаких методов setter для полей.
  • Если какое-либо из полей является изменяемым, нужно вернуть их копию вместо возврата исходного объекта.
Код с использованием Record:

public record Person(String name, int age, String email) {}
Вот и все! Всего одной строкой кода мы определили класс, который имеет те же поля, конструктор, геттеры, сеттеры и метод toString(), что и традиционный класс. Синтаксис Record позаботится обо всем шаблонном коде за нас. Из приведенного выше примера ясно, что использование записей в Java может помочь избавиться от шаблонного кода, предоставляя более краткий синтаксис для определения классов с фиксированным набором полей. По сравнению с традиционным подходом для определения и использования записей требуется меньше кода, и записи обеспечивают прямой доступ к полям с помощью методов для обновления полей. Это делает код более удобным для чтения, ремонтопригодным и менее подверженным ошибкам. Кроме того, с синтаксисом Record нам не нужно делать ничего дополнительно, чтобы сделать класс неизменяемым. По умолчанию все поля в записи являются final, а сам класс записи неизменяем. Следовательно, создание неизменяемого класса с использованием синтаксиса записи намного проще и требует меньше кода, чем традиционный подход. С записями нам не нужно объявлять поля как final, предоставлять конструктор, который инициализирует поля, или предоставлять геттеры для всех полей.

Общие классы записей

В Java также можно определить общие классы Records. Универсальный класс записи — это класс записи, который имеет один или несколько параметров типа. Перед вами пример:

public record Pair<T, U>(T first, U second) {}
В этом примере Pair — это универсальный класс записи, который принимает два параметра типа T и U. Экземпляр этой записи мы можем создать следующим образом:

Pair<String, Integer>pair = 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 {
   void print();
}

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 могут реализовывать интерфейсы точно так же, как и обычные классы. Это может быть полезно для добавления поведения к записи или для обеспечения соответствия записи контракту, определенному интерфейсом.