Gymterview
junior

Чем полезны неизменяемые объекты?

Неизменяемые (immutable) объекты — это объекты, состояние которых не может быть изменено после создания. В контексте многопоточности их главное преимущество: неизменяемый объект можно безопасно использовать из любого количества потоков без какой-либо синхронизации.

Почему это работает: все проблемы многопоточности (race condition, visibility, happens-before) связаны с изменяемым общим состоянием. Если состояние не может измениться, нет гонок, нет нужды в блокировках, нет риска увидеть «промежуточное» значение.

Правила создания неизменяемого класса:

  1. Объявите класс как final (или все конструкторы — private), чтобы предотвратить наследование с переопределением поведения.
  2. Все поля — private и final.
  3. Не предоставляйте методы, изменяющие состояние (никаких setX()).
  4. Инициализируйте все поля в конструкторе.
  5. Для мутабельных полей (коллекции, массивы, даты) делайте защитные копии — и при получении в конструкторе, и при отдаче через геттеры.
  6. Не допускайте утечки ссылки this из конструктора.
Код: пример корректного неизменяемого класса
public final class ImmutablePerson {
    private final String name;
    private final List<String> hobbies;

    public ImmutablePerson(String name, List<String> hobbies) {
        this.name = name;
        this.hobbies = List.copyOf(hobbies); // Защитная копия (Java 10+)
    }

    public String getName() {
        return name;
    }

    public List<String> getHobbies() {
        return hobbies; // List.copyOf возвращает unmodifiable list
    }
}

Примеры неизменяемых классов в JDK: String, Integer и другие обёртки примитивов, BigInteger, BigDecimal, LocalDate, LocalTime, LocalDateTime, Optional.

Использование в многопоточном контексте:

Пример
// Неизменяемый объект можно безопасно передавать между потоками
// без synchronized, volatile или AtomicReference
final ImmutablePerson person = new ImmutablePerson("Иван", List.of("Java", "Шахматы"));

ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
    pool.submit(() -> {
        // Безопасно: person не может измениться
        System.out.println(person.getName() + ": " + person.getHobbies());
    });
}

Java Records (Java 16+) — удобный способ создания неизменяемых объектов:

Пример
public record Person(String name, List<String> hobbies) {
    public Person {
        hobbies = List.copyOf(hobbies); // Защитная копия в компактном конструкторе
    }
}

Аналогия: неизменяемый объект — это напечатанная книга. Любое количество людей может читать её одновременно, не мешая друг другу. Изменяемый объект — это Google-документ: при одновременном редактировании нужна координация, иначе будет хаос.

На собеседовании часто спрашивают: «Почему String сделан неизменяемым?» Одна из причин — потокобезопасность. String активно используется как ключ в HashMap, параметр класса, имя файла и т.д. Если бы String был мутабельным, каждое его использование в многопоточной среде потребовало бы синхронизации.