1. Сравнение record и class: в чём главные отличия?
В Java у нас есть два основных способа описывать свои типы данных: через обычные классы (class) и через record-классы (record). На первый взгляд, оба варианта позволяют хранить и обрабатывать данные. Но если копнуть чуть глубже, различий больше, чем может показаться!
Таблица отличий: class vs record
| Характеристика | Обычный класс (class) | Record-класс (record) |
|---|---|---|
| Изменяемость | Любая: можно делать поля final или нет | Неизменяемый: все поля final |
| Наследование | Можно наследовать (extends), не final по умолчанию | Всегда final, не может быть суперклассом |
| Поля | Любые: статические, нестатические, final или не-final, любые типы | Только компоненты record (private final), плюс статические поля |
| Геттеры/сеттеры | Сами пишем (или генерируем Lombok'ом) | Автоматически создаются геттеры (имя поля как имя метода), сеттеров нет |
| equals/hashCode/toString | Обычно пишем вручную/генерируем (equals, hashCode, toString) | Генерируются автоматически по всем компонентам |
| Конструкторы | Любые, сколько угодно | Один основной (по всем компонентам), можно добавить компактный конструктор |
| Интерфейсы | Можно реализовывать | Можно реализовывать |
| Дополнительные методы | Любые | Можно добавлять, но только методы (не поля) |
| Использование в коллекциях | Можно, но нужно правильно реализовать equals/hashCode | Идеально подходит для ключей/значений, всё уже реализовано |
Пример для наглядности
Обычный класс:
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) { /* ... */ }
@Override
public int hashCode() { /* ... */ }
@Override
public String toString() { /* ... */ }
}
Record-класс:
public record Person(String name, int age) { }
Всё! Одна строчка кода — и мы получаем то же самое (и даже лучше). И никаких рисков забыть реализовать что-то важное.
2. Ограничения record-классов
Record-классы — это не просто «короткий синтаксис», а отдельная концепция, у которой есть свои строгие правила. Давайте рассмотрим их подробнее.
Record всегда final
Record-класс по определению всегда final. Это значит, вы не можете создать подкласс от record:
public record Point(int x, int y) { }
// public class ColoredPoint extends Point { } // Ошибка компиляции!
Если вам нужно расширять поведение, используйте обычные классы или композицию (вложите record в класс).
Record не может быть суперклассом
Record-класс не может быть родителем для других классов, он всегда final. Это логично: если бы это было возможно, кто-то мог бы добавить изменяемое поле — и вся концепция «неизменяемых данных» рухнула бы.
Только final-поля (компоненты)
Все компоненты record объявляются в заголовке и по умолчанию private final. Вы не можете добавить нестатические поля в тело record:
public record User(String login, String email) {
// int counter; // Ошибка! Нельзя добавлять нестатические поля
static int totalUsers = 0; // Можно, это статическое поле
}
Нет сеттеров
Record-класс не может иметь сеттеров для компонентов. Любая попытка добавить метод вроде setX(int x) будет бессмысленной: вы не сможете изменить значение поля после создания объекта.
public record Point(int x, int y) {
// public void setX(int x) { this.x = x; } // Ошибка: нельзя изменить final-поле
}
Нет пустого конструктора
У record-класса всегда есть только основной конструктор, принимающий значения для всех компонентов. Нельзя создать record без указания всех данных:
Point p = new Point(1, 2); // ОК
// Point p = new Point(); // Ошибка: нет конструктора без параметров
Нет нестатических инициализаторов
Record-класс не может содержать нестатические инициализаторы (те, что пишутся в фигурных скобках вне методов):
public record User(String login) {
// { /* ... */ } // Ошибка: нестатические инициализаторы запрещены
}
Ограничения по наследованию
Record-класс не может явно наследовать другой класс (кроме java.lang.Record, который скрыт от нас как базовый класс для всех record'ов). Но реализовывать интерфейсы — пожалуйста!
public interface Printable {
void print();
}
public record Book(String title) implements Printable {
@Override
public void print() {
System.out.println("Печатаем книгу: " + title);
}
}
Не подходит для сложной бизнес-логики
record — это про данные, а не про поведение. Если у вашего объекта сложная логика, изменяемое состояние, «жизненный цикл» или куча зависимостей — record тут не поможет. Лучше использовать обычный класс.
3. Когда стоит использовать record-классы?
- DTO (Data Transfer Object): для передачи неизменяемых данных между слоями приложения, сервисами, микросервисами или REST-контроллерами (например, в JSON-ответах).
- Value Object: объекты, которые определяются только своими значениями.
- Ключи и значения в коллекциях: когда важна корректная реализация equals и hashCode (например, для использования в HashMap или Set).
- Результаты вычислений: когда нужно вернуть из метода сразу несколько значений (например, record Pair<T, U>(T first, U second)).
Пример: DTO для REST-контроллера
public record UserDto(String login, String email) { }
Теперь можно смело возвращать объект этого типа из контроллера, не боясь, что кто-то изменит его поля.
Пример: Ключ для HashMap
public record Point(int x, int y) { }
Map<Point, String> pointNames = new HashMap<>();
pointNames.put(new Point(1, 2), "A");
pointNames.put(new Point(3, 4), "B");
// Всё работает корректно: equals и hashCode уже реализованы!
4. Когда record-классы использовать НЕ стоит
- Изменяемое состояние: если хотя бы одно поле должно меняться после создания объекта.
- Сложная логика: если у объекта сложное поведение, много методов, вложенные объекты с изменяемым состоянием.
- Наследование: если требуется иерархия классов, абстрактные базовые классы, переопределение методов.
- Сущности бизнес-логики: например, объекты, которые живут в базе данных и имеют уникальный идентификатор.
Пример: когда нужен обычный класс
public class Account {
private String id;
private int balance;
public Account(String id, int balance) {
this.id = id;
this.balance = balance;
}
public void deposit(int amount) { balance += amount; }
public void withdraw(int amount) { balance -= amount; }
// getters, setters, equals, hashCode, toString...
}
Здесь явно видно, что состояние объекта меняется — record не подходит.
5. Практические примеры: выбираем между record и class
Пример 1: record — идеальный выбор
public record Rectangle(int width, int height) {
public int area() {
return width * height;
}
}
- Прямоугольник определяется только шириной и высотой.
- Нет необходимости менять эти значения после создания.
- Можно добавить полезный метод area().
- Всё остальное Java сделает за вас.
Пример 2: class — лучший вариант
public class MutableRectangle {
private int width;
private int height;
public MutableRectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int area() { return width * height; }
}
Нужно изменять размеры прямоугольника после создания? Используйте обычный класс.
6. Типичные ошибки при работе с record-классами
Ошибка №1: попытка добавить нестатическое поле.
Record-класс не позволяет объявлять нестатические поля вне списка компонентов. Если попытаться — компилятор выдаст ошибку. Например:
public record City(String name) {
// int population; // Ошибка!
}
Ошибка №2: желание добавить сеттер.
Record не поддерживает сеттеры для компонентов. Любая попытка изменить значение поля после создания объекта — ошибка компиляции.
Ошибка №3: попытка наследовать record или от record.
Record всегда final. Нельзя наследовать от record, и record не может наследовать другой класс (кроме скрытого java.lang.Record).
Ошибка №4: использование record для mutable-объектов.
Если вы планируете менять состояние объекта после создания — record не для вас! Используйте обычный класс.
Ошибка №5: забыли про ограничения конструктора.
У record-класса обязательно должен быть конструктор, принимающий значения для всех компонентов. Конструктора без параметров нет!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ