Неизменяемость
Неизменяемый (immutable) объект после создания нельзя поменять: все его поля остаются такими, какими их задал конструктор. Чтобы «изменить» такой объект, создают новый. С неизменяемыми объектами проще работать: их можно без опаски передавать в любые методы и потоки — никто не испортит данные у вас за спиной.
Неизменяемые классы в Java
Вы уже пользовались неизменяемыми объектами. String, Integer, LocalDate не меняются — их методы возвращают новые объекты:
void main() {
String name = "анна";
String upper = name.toUpperCase();
IO.println(name);
IO.println(upper);
}Результат:
анна
АННАtoUpperCase() не изменил строку name, а вернул новую.
Свой неизменяемый класс
Чтобы класс был неизменяемым, все его поля объявляют final (присвоить их можно только в конструкторе), сеттеров не делают, а «изменяющие» методы возвращают новый объект:
final class Money {
private final int cents;
Money(int cents) {
this.cents = cents;
}
Money plus(Money other) {
return new Money(cents + other.cents);
}
@Override
public String toString() {
return "$" + cents / 100 + "." + String.format("%02d", cents % 100);
}
}
void main() {
Money price = new Money(1250);
Money total = price.plus(new Money(399));
IO.println(price);
IO.println(total);
}Результат:
$12.50
$16.49Слово final у самого класса запрещает наследование: иначе подкласс мог бы добавить изменяемые поля.
Записи
Записи (records) неизменяемы по умолчанию: их поля автоматически final, а сеттеров нет. Это самый короткий способ создать неизменяемый класс. «Изменение» делают через создание новой записи:
record Point(int x, int y) {
Point withX(int newX) {
return new Point(newX, y);
}
}
void main() {
Point start = new Point(1, 2);
Point moved = start.withX(10);
IO.println(start);
IO.println(moved);
}Результат:
Point[x=1, y=2]
Point[x=10, y=2]Неизменяемые коллекции
List.of, Set.of и Map.of создают неизменяемые коллекции. Любая попытка изменить их выбрасывает исключение. List.copyOf делает неизменяемую копию существующего списка:
void main() {
List<String> source = new ArrayList<>(List.of("чай", "кофе"));
List<String> copy = List.copyOf(source);
source.add("какао");
IO.println(source);
IO.println(copy);
}Результат:
[чай, кофе, какао]
[чай, кофе]Копия не изменилась вместе с исходным списком. Есть и Collections.unmodifiableList(source) — но это не копия, а представление: менять через него нельзя, а изменения в source в нём видны.
Защитная копия
Если объект получает изменяемый список, его поле final не спасает: снаружи список по-прежнему можно изменить. Поэтому в конструкторе делают защитную копию:
record Team(String name, List<String> members) {
Team {
members = List.copyOf(members);
}
}
void main() {
List<String> people = new ArrayList<>(List.of("Анна", "Олег"));
Team team = new Team("Бэкенд", people);
people.add("Ира");
IO.println(team.members());
}Результат:
[Анна, Олег]Компактный конструктор записи заменил переданный список неизменяемой копией. Добавление Иры в исходный список команду не затронуло.
Частая ошибка
final у переменной запрещает присвоить ей другой объект, но не мешает менять сам объект. Список, объявленный final, можно пополнять сколько угодно, а вот List.of действительно изменить нельзя:
void main() {
final List<String> tasks = new ArrayList<>();
tasks.add("Купить хлеб");
IO.println(tasks);
List<String> fixed = List.of("Купить хлеб");
fixed.add("Позвонить Олегу");
}Результат:
[Купить хлеб]
Exception in thread "main" java.lang.UnsupportedOperationException
at Main.main(Main.java:7)У UnsupportedOperationException от List.of нет сообщения — видно только тип исключения. Неизменяемость — свойство объекта, а не переменной: final защищает только ссылку.