1. Record-классы и неизменяемость
Неизменяемость — это свойство объекта, при котором его состояние не может быть изменено после создания. Другими словами: если объект создан, вы не можете поменять его внутренние данные. Всё! Как в камне высечено.
Представьте билет на поезд. Как только он напечатан, вы не можете изменить дату или место отправления (ну, если не брать в расчёт хитрые махинации с фотошопом). Билет — неизменяемый объект. Если вы хотите другой билет — покупаете новый.
В программировании такие объекты называют immutable objects. Они защищают программу от случайных изменений, делают код более безопасным и предсказуемым.
Признаки неизменяемого объекта
- Все поля объекта — final (их можно присвоить только один раз, обычно в конструкторе).
- Нет сеттеров (методов, которые меняют значения полей).
- Все методы, возвращающие внутренние данные, либо возвращают копии, либо сами данные тоже неизменяемы.
Record-классы в Java были придуманы именно для того, чтобы создавать неизменяемые структуры данных просто и без боли.
Почему record неизменяемый?
- Все компоненты record — final.
Когда вы объявляете record, компилятор автоматически делает все его поля private final. Это значит, что после создания объекта вы не сможете изменить его поля. - Нет сеттеров.
В классе-record нельзя добавить метод setX(int x) — компилятор не позволит вам изменить значение поля после создания объекта. - Конструктор присваивает значения только один раз.
Все значения задаются только в момент создания объекта.
Пример
public record Point(int x, int y) {}
Point p = new Point(5, 10);
// p.x = 7; // Ошибка компиляции: поле x имеет private-доступ и является final
// p.x(7); // Ошибка: нет сеттера!
System.out.println(p.x()); // 5
Попытка изменить поле или вызвать несуществующий сеттер приводит к ошибке компиляции. Java строго следит за контрактом неизменяемости.
2. Преимущества неизменяемых объектов
Безопасность в многопоточности
В многопоточных программах (а таких сейчас большинство!) неизменяемые объекты — это как бронежилет. Если объект нельзя изменить, разные потоки могут его спокойно читать, не боясь, что кто-то в этот момент поменяет данные. Не нужно синхронизировать доступ и переживать из-за гонок данных.
Факт: многие классы в стандартной библиотеке Java, которые активно используются в многопоточных сценариях, либо неизменяемы, либо специально защищены от изменений.
Упрощение понимания кода
Если объект неизменяем, вы всегда уверены: передали его в другой метод или класс — и он не изменится «за вашей спиной». Это сильно облегчает чтение и отладку кода. Не нужно гадать, кто и где мог поменять поле, — никто не мог!
Удобно использовать как ключи в коллекциях
Неизменяемые объекты отлично подходят для использования в качестве ключей в коллекциях вроде HashMap или HashSet. Почему? Потому что их equals и hashCode зависят только от полей, которые не меняются. Значит, объект не «потеряется» в коллекции из-за того, что его состояние изменилось.
Меньше скрытых багов
Изменяемый объект легко испортить, случайно передав ссылку не туда. Неизменяемый же объект — как печатная книга: никто не может вырвать или переписать страницу.
3. Сравнение с обычными классами
Давайте сравним поведение обычного класса и record-класса. Для примера возьмём простую модель точки на плоскости.
Обычный класс (mutable)
public class PointClass {
private int x;
private int y;
public PointClass(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
}
Можно создать объект и потом сколько угодно менять его состояние:
PointClass p = new PointClass(1, 2);
p.setX(10); // p.x теперь 10
Record-класс (immutable)
public record Point(int x, int y) {}
Создаёте объект — и всё, он навсегда такой, каким был создан:
Point p = new Point(1, 2);
// p.x = 10; // Ошибка! Нет доступа к полю
// p.x(10); // Ошибка! Нет сеттера
Таблица: сравнение поведения
| Обычный класс | Record-класс | |
|---|---|---|
| Поля | любые | только private final |
| Сеттеры | можно добавить | нельзя добавить |
| Изменяемость | изменяемый (mutable) | неизменяемый |
| Автогенерация | нет | да (equals, hashCode, toString) |
4. Практика: как использовать неизменяемость record-классов
Давайте попробуем применить record-класс в небольшом примере. Пусть у нас есть банковское приложение, и мы хотим хранить информацию о транзакции:
public record Transaction(String fromAccount, String toAccount, double amount) {}
Создаём объект:
Transaction t = new Transaction("12345", "67890", 1500.0);
System.out.println(t);
// Transaction[fromAccount=12345, toAccount=67890, amount=1500.0]
Попробуем "перевести" деньги на другой счёт:
// t.toAccount = "11111"; // Ошибка! Поле final, доступа нет
// t.toAccount("11111"); // Ошибка! Нет сеттера
Если нам нужна другая транзакция — создаём новый объект:
Transaction t2 = new Transaction(t.fromAccount(), "11111", t.amount());
Важно: неизменяемость не значит «неудобно». Это просто другой стиль работы: если нужно новое состояние — создаём новый объект.
5. Особенности неизменяемости: что нужно помнить
Неизменяемость — не всегда абсолютна!
Record-класс гарантирует, что его поля не изменятся. Но если поле — это ссылка на изменяемый объект (например, массив или обычный класс), то содержимое этого объекта может быть изменено.
Пример с массивом
public record DataHolder(int[] data) {}
int[] arr = {1, 2, 3};
DataHolder holder = new DataHolder(arr);
arr[0] = 99;
System.out.println(holder.data()[0]); // 99! Массив изменился
Вывод: если хотите настоящую неизменяемость, используйте только неизменяемые типы (String, Integer, другие record’ы и т.д.) или делайте копии изменяемых объектов внутри канонического конструктора record-класса. Например:
int[] copy = Arrays.copyOf(data, data.length);
6. Как самому сделать обычный класс неизменяемым
Если вы хотите сделать обычный класс immutable (неизменяемым), придётся вручную:
- Пометить все поля как private final,
- Не добавлять сеттеров,
- Все поля инициализировать только через конструктор,
- Если поле — изменяемый объект, делать защитную копию (defensive copy).
Пример
public final class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String name() {
return name;
}
public int age() {
return age;
}
}
Согласитесь, с record-классом это делается проще и короче:
public record User(String name, int age) {}
7. Типичные ошибки при работе с неизменяемыми record-классами
Ошибка №1: попытка изменить поле после создания.
Новички часто пытаются написать p.x = 42; или p.x(42); для record-объекта. Но компилятор сразу скажет: «Так нельзя! Поле final, сеттера нет».
Ошибка №2: использование изменяемых объектов как компонентов record.
Если вы добавили в record поле типа List, Map, массив или другой изменяемый объект, то сам record не защитит вас от изменений содержимого этого объекта. Например, если у вас есть record User(List<String> hobbies), то кто-то может добавить или удалить элемент из списка, и это изменит состояние вашего record-объекта. Чтобы этого избежать, используйте неизменяемые коллекции (List.copyOf, Collections.unmodifiableList) или делайте копии коллекций внутри конструктора record.
Ошибка №3: неверное понимание неизменяемости.
Некоторые думают, что если объект — record, то он защищён от любых изменений. На самом деле, если поля — ссылки на изменяемые объекты, их содержимое можно поменять, и это приведёт к неожиданным багам.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ