JavaRush /Курси /JAVA 25 SELF /Record: синтаксис і переваги

Record: синтаксис і переваги

JAVA 25 SELF
Рівень 22 , Лекція 0
Відкрита

1. Проблема «класів‑DTO»: навіщо нам record‑класи?

Відверто: скільки разів ви писали ось такий клас?


public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point{" + "x=" + x + ", y=" + y + '}';
    }
}

Здається, нічого складного, але порахуйте рядки коду! А тепер уявіть, що у вас 10 таких класів, і в кожному — по 5–6 полів. Навіть IDE втомлюється генерувати цей шаблонний код. А якщо ви вирішите додати нове поле — доведеться редагувати конструктор, гетер, equals, hashCode, toString… Нудно, рутинно — і це джерело помилок.

Такі класи називають DTO (Data Transfer Object) або Value Object. Вони просто зберігають дані — та й годі. Але через шаблонний код їх важко підтримувати.

Якщо вам здається, що це не проблема, просто дочекайтеся моменту, коли доведеться змінювати 50 таких класів одночасно. Тоді ви з особливим теплом згадаєте про record‑класи!

2. Вступ до record: синтаксис і «магія» Java 16+

Починаючи з Java 16, усе змінилося. З’явився новий вид класів — record. Вони спеціально створені для випадків, коли потрібно просто зберігати набір даних. Синтаксис — майже як у кортежах в інших мовах.

Як оголосити record?


public record Point(int x, int y) { }

І… усе! Ви щойно створили незмінюваний клас із двома полями, конструктором, аксесорами, equals, hashCode і toString. Без зайвої писанини.

Що робить Java «за лаштунками»?

  • Поля x і y стають private final.
  • Аксесори створюються автоматично: int x() і int y().
  • Конструктор: public Point(int x, int y).
  • equals/hashCode: порівнюють усі поля за значенням.
  • toString: повертає рядок на кшталт "Point[x=1, y=2]".

Можна сказати, що record — це «DTO на стероїдах»: менше коду, більше гарантій, менше помилок.

Незмінність (immutability)

Усі поля record‑класу автоматично final. Після створення об’єкта змінити його не можна — це гарантує компілятор.

Якщо ви спробуєте додати сетер або зробити поле не final — компілятор зупинить вас. Така турбота про ваш спокій трапляється нечасто!

3. Приклад використання record‑класу

Звичайний клас (багато коду):


public class Client {
    private final String name;
    private final int id;

    public Client(String name, int id) {
        this.name = name;
        this.id = id;
    }

    public String getName() { return name; }
    public int getId() { return id; }

    // equals, hashCode, toString ...
}

Record‑клас (один рядок!):


public record Client(String name, int id) { }

Використання:


public class Main {
    public static void main(String[] args) {
        Client client = new Client("Іван", 123);
        System.out.println(client.name()); // Іван
        System.out.println(client.id());   // 123
        System.out.println(client);        // Client[name=Іван, id=123]
    }
}

Зверніть увагу:
- Методи доступу до полів називаються так само, як поля: name(), id().
- Немає setName() або setId() — об’єкт не можна змінити після створення.

4. Переваги record‑класів: менше коду, менше помилок

Менше коду — більше щастя

Навіщо писати 40 рядків коду, якщо можна обійтися одним? Record‑класи заощаджують час і нерви, особливо у великих проєктах, де багато DTO та value‑об’єктів.

Незмінність «за контрактом»

  • Record‑класи завжди final і незмінювані.
  • Об’єкт не можна підмінити або випадково змінити.
  • Немає «дивних» помилок через зміну стану об’єкта в неочікуваному місці.
  • Можна безпечно використовувати у багатопотокових програмах (якщо всі поля також незмінювані).

Автоматична генерація equals/hashCode/toString

Не потрібно вручну писати методи порівняння, обчислення хешу й гарного виведення. Усе робиться автоматично і правильно.


Client c1 = new Client("Анна", 42);
Client c2 = new Client("Анна", 42);

System.out.println(c1.equals(c2));                 // true
System.out.println(c1.hashCode() == c2.hashCode()); // true
System.out.println(c1);                             // Client[name=Анна, id=42]

Ідеально для колекцій і ключів

Record‑об’єкти можна використовувати як ключі в HashMap, елементи в HashSet тощо — усе працюватиме коректно, адже equals і hashCode враховують усі поля.


import java.util.HashMap;
import java.util.Map;

Map<Client, String> clients = new HashMap<>();
clients.put(new Client("Анна", 42), "VIP");

System.out.println(clients.get(new Client("Анна", 42))); // VIP

Чіткий опис даних

Синтаксис record‑класу одразу показує, які дані зберігаються і що об’єкт незмінюваний. Це робить код зрозумілішим для інших розробників (і для вас за пів року).

5. Таблиця: порівняння звичайного класу та record‑класу

Звичайний клас Record‑клас
Синтаксис Багато коду Один рядок
Незмінюваність Потрібно визначати вручну Гарантована компілятором
Автогенерація методів Ні Так (equals, hashCode, toString)
Можна додати поля Так Лише компоненти record
Успадкування Можна успадковувати Завжди final, успадковувати не можна
Використання у колекціях Потрібно коректно реалізувати методи Працює без додаткового коду

6. Розвиваємо навчальний застосунок: приклад з record‑класом

Припустімо, у вашому навчальному банківському застосунку потрібно зберігати операції за рахунком: дата, сума, тип операції (наприклад, «поповнення» або «зняття»).

До Java 16:


public class Transaction {
    private final LocalDate date;
    private final double amount;
    private final String type;

    public Transaction(LocalDate date, double amount, String type) {
        this.date = date;
        this.amount = amount;
        this.type = type;
    }

    public LocalDate getDate() { return date; }
    public double getAmount() { return amount; }
    public String getType() { return type; }

    // equals, hashCode, toString ...
}

Починаючи з Java 16 — record:


import java.time.LocalDate;

public record Transaction(LocalDate date, double amount, String type) { }

Використання:


Transaction t = new Transaction(LocalDate.now(), 100.0, "deposit");
System.out.println(t);         // Transaction[date=2024-06-01, amount=100.0, type=deposit]
System.out.println(t.amount()); // 100.0

7. Візуалізація: що генерує record

Погляньмо на «розгорнутий» record‑клас (приблизно те, що згенерує компілятор):


public final class Point extends java.lang.Record {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int x() { return x; }
    public int y() { return y; }

    @Override
    public boolean equals(Object o) { /* порівняння за полями */ }

    @Override
    public int hashCode() { /* обчислення за полями */ }

    @Override
    public String toString() { /* гарне виведення */ }
}

Коротко: коли варто використовувати record

  • Коли потрібен незмінюваний об’єкт із набором даних.
  • Коли потрібен «чесний» equals/hashCode/toString без ручної писанини.
  • Коли ви робите DTO, Value Object, пари, трійки, колір, точку, діапазон, ключ для колекції тощо.

8. Типові помилки під час роботи з record‑класами

Помилка № 1: спроба додати сетер або змінити поле після створення.
Record‑клас не дозволяє змінювати свої поля. Якщо ви спробуєте додати метод на кшталт setX(int x), компілятор одразу скаже «не можна». Те саме — якщо спробувати змінити поле напряму.

Помилка № 2: спроба додати нестатичне поле.
У record‑класі можна оголошувати лише компоненти (поля, вказані у дужках після імені record) і статичні поля. Звичайні нестатичні поля додавати не можна — компілятор цього не дозволить.

Помилка № 3: використання record для логіки зі змінним станом.
Record‑класи не призначені для об’єктів зі змінним станом. Якщо вам потрібно щось змінювати після створення — використовуйте звичайний клас.

Помилка № 4: забули, що record завжди final.
Record‑клас не можна успадковувати й не можна зробити його суперкласом. Спроба порушити це обмеження призведе до помилки компіляції. Ключове: не намагайтеся розширювати record — його задумано як завершений незмінюваний тип.

Помилка № 5: ігнорування автогенерованих методів.
Якщо ви перевизначаєте equals, hashCode або toString, будьте обережні — не порушуйте їхній контракт, інакше колекції та порівняння працюватимуть некоректно.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ