null

Поговорим о Java Records

Что такое 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.

Выражаю этому автору большую благодарность.