«1. Введение

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

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

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

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

2. Цель

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

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

Для этого мы создаем классы данных со следующим:

  1. private, final field for each piece of data
  2. getter for each field
  3. public constructor with a corresponding argument for each field
  4. equals method that returns true for objects of the same class when all fields match
  5. hashCode method that returns the same value when all fields match
  6. toString method that includes the name of the class and the name of each field and its corresponding value

Например, мы можем создать простой класс данных Person с именем и адресом:

public class Person {

    private final String name;
    private final String address;

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

    @Override
    public int hashCode() {
        return Objects.hash(name, address);
    }

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

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

    // standard getters
}

Хотя это и достигает нашей цели, есть с ним две проблемы:

  1. There is a lot of boilerplate code
  2. We obscure the purpose of our class – to represent a person with a name and address

В первом случае мы должны повторять один и тот же утомительный процесс для каждого класса данных, монотонно создавая новое поле для каждого фрагмента данных, создавая методы equals, hashCode и toString и создавая конструктор, который принимает каждое поле.

Хотя IDE могут автоматически генерировать многие из этих классов, они не могут автоматически обновлять наши классы, когда мы добавляем новое поле. Например, если мы добавляем новое поле, нам нужно обновить наш метод equals, чтобы включить это поле.

Во втором случае лишний код скрывает, что наш класс — это просто класс данных с двумя строковыми полями: имя и адрес.

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

3. Основы

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

Методы equals, hashCode и toString, а также поля private, final и открытый конструктор генерируются компилятором Java.

Чтобы создать запись о человеке, мы используем ключевое слово записи:

public record Person (String name, String address) {}

3.1. Конструктор

Используя записи, для нас генерируется общедоступный конструктор — с аргументом для каждого поля.

В случае нашей записи Person эквивалентный конструктор:

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

Этот конструктор можно использовать так же, как класс, для создания объектов из записи:

Person person = new Person("John Doe", "100 Linda Ln.");

3.2. Геттеры

Мы также бесплатно получаем общедоступные методы геттеров, чьи имена совпадают с именем нашего поля.

В нашей записи Person это означает геттер name() и address():

@Test
public void givenValidNameAndAddress_whenGetNameAndAddress_thenExpectedValuesReturned() {
    String name = "John Doe";
    String address = "100 Linda Ln.";

    Person person = new Person(name, address);

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

3.3. equals

Дополнительно для нас генерируется метод equals.

Этот метод возвращает true, если предоставленный объект относится к одному типу и значения всех его полей совпадают:

@Test
public void givenSameNameAndAddress_whenEquals_thenPersonsEqual() {
    String name = "John Doe";
    String address = "100 Linda Ln.";

    Person person1 = new Person(name, address);
    Person person2 = new Person(name, address);

    assertTrue(person1.equals(person2));
}

Если какие-либо поля различаются между двумя экземплярами Person, метод equals вернет false.

3.4. hashCode

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

Наш метод hashCode возвращает одно и то же значение для двух объектов Person, если все значения полей для обоих объектов совпадают (за исключением коллизий из-за парадокса дня рождения):

@Test
public void givenSameNameAndAddress_whenHashCode_thenPersonsEqual() {
    String name = "John Doe";
    String address = "100 Linda Ln.";

    Person person1 = new Person(name, address);
    Person person2 = new Person(name, address);

    assertEquals(person1.hashCode(), person2.hashCode());
}

Значение hashCode будет отличаться, если любое из полей значения различаются.

3.5. toString

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

Таким образом, создание экземпляра Person с именем «John Doe» и адресом «100 Linda Ln.» приводит к следующему результату toString:

Person[name=John Doe, address=100 Linda Ln.]

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

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

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

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

public record Person(String name, String address) {
    public Person {
        Objects.requireNonNull(name);
        Objects.requireNonNull(address);
    }
}

«

public record Person(String name, String address) {
    public Person(String name) {
        this(name, "Unknown");
    }
}

«Мы также можем создавать новые конструкторы с другими аргументами, указав другой список аргументов:

Как и в случае с конструкторами классов, на поля можно ссылаться с помощью ключевого слова this (например, this.name и this.address) и аргументы соответствуют имени полей (то есть имени и адресу).

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

Обратите внимание, что создание конструктора с теми же аргументами, что и сгенерированный общедоступный конструктор, допустимо, но для этого требуется, чтобы каждое поле было инициализировано вручную:

Кроме того, объявление конструктора без аргументов и конструктора со списком аргументов сопоставление сгенерированного конструктора приводит к ошибке компиляции.

public record Person(String name, String address) {
    public Person {
        Objects.requireNonNull(name);
        Objects.requireNonNull(address);
    }
    
    public Person(String name, String address) {
        this.name = name;
        this.address = address;
    }
}

Поэтому следующее не будет компилироваться:

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

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

public record Person(String name, String address) {
    public static String UNKNOWN_ADDRESS = "Unknown";
}

Мы объявляем статические переменные, используя тот же синтаксис, что и класс:

public record Person(String name, String address) {
    public static Person unnamed(String address) {
        return new Person("Unnamed", address);
    }
}

Точно так же мы объявляем статические методы, используя тот же синтаксис, что и класс:

Person.UNKNOWN_ADDRESS
Person.unnamed("100 Linda Ln.");

Затем мы можем ссылаться как на статические переменные, так и на статические методы, использующие имя записи:

6. Заключение

В этой статье мы рассмотрели ключевое слово записи, появившееся в Java 14, включая его основные понятия и тонкости.

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