Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Неизменяемость

Неизменяемый (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 защищает только ссылку.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.