Статья

Ключевое слово Record в Java

Задача передачи неизменяемых данных между объектами является одной из наиболее распространенных и обыденных задач во многих Java-приложениях.

До Java 14 это требовало создания класса с шаблонными полями и методами, которые были подвержены тривиальным ошибкам и запутанным намерениям.

С выпуском Java 14 мы теперь можем использовать записи для устранения этих проблем.

В этой статье мы рассмотрим основы записей, включая их назначение, сгенерированные методы и методы настройки.
Обычно мы пишем классы просто для хранения данных, таких как результаты выборки из базы данных, результаты запроса или данные из сервиса.
Во многих случаях эти данные неизменяемы, поскольку неизменяемость обеспечивает достоверность данных без синхронизации при использовании мультипоточности.

Для достижения этой цели обычно мы создаем data-классы, в которых:
  1. все поля данных с модификаторамиprivate, final
  2. есть getter для каждого поля
  3. public конструктор with с соответствующим аргументом для каждого поля
  4. метод equals, возвращающий true для объектов одного класса с одинаковыми полями
  5. метод hashCode, возвращающий одно и то же значение для разных объектов, когда все поля равны
  6. метод toString, включающий имя класса и имя каждого поля
К примеру, мы можем создать простой класс данных User с именем и адресом:
public class User {

    private final String name;
    private final String address;

    public User(String name, String address) {
        this.name = name;
        this.address = address;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        } else if (!(obj instanceof User)) {
            return false;
        } else {
            User other = (User) obj;
            return Objects.equals(name, other.name)
              && Objects.equals(address, other.address);
        }
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, address);
    }



    @Override
    public String toString() {
        return "User [name=" + name + ", address=" + address + "]";
    }

    // геттеры...
}
Здесь есть две проблемы:
  1. Много шаблонного кода
  2. Мы совершаем много лишней работы, кроме основной цели: представить поьзователя с именем и адресом

В первом случае мы должны повторять один и тот же утомительный процесс для каждого data-класса, монотонно создавая новое поле для каждого фрагмента данных; методы equals, hashCode и toString; и конструктор, который принимает каждое поле.
Хотя IDE могут автоматически генерировать многие из этих классов, они не могут автоматически обновлять наши классы, когда мы добавляем новые поля. К примеру, если мы добавили новое поле, нужно обновить метод equals, чтобы включить это поле в него.

Лучшим решением было бы явно объявить, что наш класс является классом данных.

Основы

Начиная с JDK 14, мы можем заменить наши повторяющиеся классы данных записями - Record. Записи - это неизменяемые data-классы, для создания которых требуются только тип и имена полей.

Методы equals, hashCode и toString, неизменяемые поля и открытый конструктор генерируются компилятором Java.
Чтобы создать запись о пользователе, используем ключевое слово record:
public record User (String name, String address) {}

Конструктор

При использовании записей, генерируется public конструктор с аргументом для каждого поля.

В случае нашей записи User эквивалентным конструктором будет:
public User(String name, String address) {
    this.name = name;
    this.address = address;
}
Этот конструктор можно использовать таким же образом, как и класс, для создания экземпляров объектов из записи:
User user = new User("Abdrew", "3/1 Lake St.");

Геттеры

Мы также бесплатно получаем public геттеры, имена которых совпадают с названиями полей.

В нашей записи User:
@Test
public void runTest() {
    String name = "Andrew";
    String address = "3/1 Lake St.";

    User user = new User(name, address);

    assertEquals(name, user.name());
    assertEquals(address, user.address());
}

equals

Кроме того, для нас генерируется метод equals.

Этот метод возвращает значение true, если переданный объект имеет тот же тип и значения всех его полей совпадают:
@Test
public void runTest() {
    String name = "Andrew";
    String address = "3/1 Lake St.";

    User user1 = new User(name, address);
    User user2 = new User(name, address);

    assertTrue(user1.equals(user2));
}
Если какое-либо из полей отличается в двух экземплярах User, метод equals вернет значение false.

hashCode

Аналогично методу equals, для нас также генерируется соответствующий метод hashCode.

Наш метод hashCode возвращает одинаковое значение для двух объектов User, если все значения полей для обоих объектов совпадают:
@Test
public void runTest() {
    String name = "Andrew";
    String address = "3/1 Lake St.";

    User user1 = new User(name, address);
    User user2 = new User(name, address);

    assertEquals(user1.hashCode(), user2.hashCode());
}
Значение хэшкода, скорее всего, будет отличаться, если будут отличаться значения каких-либо полей. Но это не гарантируется контрактом hashCode().

toString

Наконец, для нас также генерируется метод toString, результатом которого является строка, содержащая имя записи, за которой следует название каждого поля и соответствующее ему значение в квадратных скобках.

Следовательно, создание экземпляра User с именем “Andrew” и адресом ”3/1 Lake St." приводит к следующему результату toString:
User[name=Andrew, address=3/1 Lake St.]

Конструкторы

Хотя для нас сгенерирован publc конструктор, мы все равно можем настроить реализацию нашего конструктора.

Эта настройка предназначена для проверки и должна быть максимально простой.

Например, мы можем убедиться, что имя и адрес, указанные в нашей записи User, не являются null, используя следующую реализацию конструктора:
public record User(String name, String address) {
    public User {
        Objects.requireNonNull(name);
        Objects.requireNonNull(address);
    }
}
Мы также можем создавать новые конструкторы, предоставляя другой список аргументов:
public record User(String name, String address) {
    public User(String name) {
        this(name, "Unknown");
    }
}
Как и в случае с конструкторами классов, на поля можно ссылаться с помощью ключевого слова this (например, this.name и this.address), а аргументы соответствуют именам полей (то есть имени и адресу).

Обратите внимание, что создание конструктора с теми же аргументами, что и сгенерированный открытый конструктор, допустимо, но для этого требуется, чтобы каждое поле было инициализировано вручную:
public record User(String name, String address) {
    public User(String name, String address) {
        this.name = name;
        this.address = address;
    }
}
Кроме того, объявление вместе компактного конструктора и конструктора со списком аргументов, соответствующим сгенерированному конструктору, приводит к ошибке компиляции.

Следовательно, следующий код не будет скомпилирован:
public record User(String name, String address) {
    public User {
        Objects.requireNonNull(name);
        Objects.requireNonNull(address);
    }
    
    public User(String name, String address) {
        this.name = name;
        this.address = address;
    }
}

Статические переменные и методы

Как и в случае с обычными классами Java, мы также можем включать статические переменные и методы в наши записи.

Мы объявляем статические переменные, используя тот же синтаксис, что и класс:
public record User(String name, String address) {
    public static String UNKNOWN_ADDRESS = "Unknown";
}
Аналогично, мы объявляем статические методы, используя тот же синтаксис, что и класс:
public record User(String name, String address) {
    public static User unnamed(String address) {
        return new User("Unnamed", address);
    }
}
Затем мы можем ссылаться как на статические переменные, так и на статические методы, используя имя записи:
User.UNKNOWN_ADDRESS
User.unnamed("10 Moscow Ln.");

Заключение

В этой статье мы рассмотрели ключевое слово record, введенное в Java 14, включая фундаментальные концепции и тонкости.

Используя records с их методами, генерируемыми компилятором, мы можем сократить количество шаблонного кода и повысить надежность наших неизменяемых классов.
java