JavaRush /Java блог /Random UA /Кава-брейк #128. Посібник з використання Java Records

Кава-брейк #128. Посібник з використання Java Records

Стаття з групи Random UA
Джерело: abhinavpandey.dev У цьому посібнику ми розглянемо основи використання записів (Records) в Java. Записи з'явабося в Java 14 як спосіб видалити шаблонний код навколо створення об'єктів-значень (Value objects), використовуючи переваги незмінних об'єктів. Кава-брейк #128.  Посібник з використання Java Records - 1

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

Об'єкти-значення мають бути незмінними. Це означає, що ми маємо обмежити способи зміни полів об'єкта. Це доцільно з таких причин:
  • Щоб уникнути ризику випадкової зміни значення поля.
  • Щоб переконатися, що рівні об'єкти залишаються однаковими протягом усього їхнього життя.
Оскільки клас Contact вже незмінюємо, тепер ми:
  1. зробабо поля private та final .
  2. надали тільки getter для кожного поля (без setters ).

1.4. Реєстрація об'єктів Value objects

Часто нам потрібно реєструвати значення, які у об'єктах. Це робиться шляхом надання методу toString . Щоразу, коли об'єкт реєструється чи друкується, викликається метод toString . Тут найпростіший спосіб роздрукувати значення кожного поля. Ось приклад:
public class Contact {
    // ...
    @Override
    public String toString() {
        return "Contact[" +
                "name='" + name + '\'' +
                ", email=" + email +
                ']';
    }
}

2. Скорочення шаблонів за допомогою Records

Оскільки більшість об'єктів-значень мають однакові потреби та функціональні можливості, було б непогано спростити процес їх створення. Давайте подивимося, як записи допомагають досягти цього.

2.1. Перетворення класу Person на Record

Давайте створимо запис класу Contact , який має ту ж функціональність, що і клас Contact , визначений вище.
public record Contact(String name, String email) {}
Ключове слово record використовується для створення класу Record . Записи можуть оброблятися зухвалою стороною так само, як клас. Наприклад, щоб створити новий екземпляр запису, ми можемо використати ключове слово new .
Contact contact = new Contact("John Doe", "johnrocks@gmail.com");

2.2. Типова поведінка

Ми скоротабо код до одного рядка. Перерахуємо, що до нього входить:
  1. Поля name та email є private та final за замовчуванням.

  2. Код визначає "канонічний конструктор", який приймає поля як параметри.

  3. Поля доступні через гетер-подібні методи — name() та email() . Для полів немає установника, тому дані об'єкті стають незмінними.

  4. Реалізовано метод toString для друку полів так само, як ми робабо це для класу Contact .

  5. Реалізовані методи equals та .hashCode . Вони включають усі поля, як і клас Contact .

2.3 Канонічний конструктор

Конструктор, визначений за замовчуванням, приймає всі поля як вхідні параметри і встановлює їх у поля. Наприклад, нижче показаний канонічний конструктор (Canonical Constructor), визначений за умовчанням:
public Contact(String name, String email) {
    this.name = name;
    this.email = 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<Contact> {
    @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]{2,6}$", 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]{2,6}$", Pattern.CASE_INSENSITIVE);

    static Pattern emailRegex() {
        return EMAIL_REGEX_PATTERN;
    }
}
Зауважте, що в статичних полях немає неявних обмежень. За потреби вони можуть бути загальнодоступними та не остаточними.

Висновок

Записи – чудовий спосіб визначити класи даних. Вони набагато зручніші та потужніші, ніж підхід JavaBeans/POJO. Через простоту реалізації їм слід віддавати перевагу над іншими способами створення об'єктів-значень.
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ