Какие преимущества и недостатки у неизменяемых (immutable) классов в Java?

«Какие преимущества и недостатки у неизменяемых (immutable) классов в Java?» — вопрос из категории Java Core, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Преимущества:

  1. Потокобезопасность без синхронизации: Поскольку состояние объекта фиксировано после создания, его можно свободно передавать между потоками. Это устраняет целый класс ошибок, связанных с состоянием гонки (race condition).
  2. Простота и надежность: Отсутствие побочных эффектов делает код предсказуемым, упрощает отладку и рассуждения. Объект не может быть случайно изменен в другом месте программы.
  3. Безопасное кэширование: Можно кэшировать и свободно повторно использовать экземпляры. Классический пример — пулы строк или перечисления (enum). Хэш-код можно вычислить один раз при создании и сохранить.
  4. Идеальные ключи для коллекций: Неизменяемость гарантирует, что hashCode() ключа в HashMap или HashSet останется постоянным, что является критическим требованием для корректной работы этих коллекций.

Недостатки:

  1. Потребление памяти: Каждое "изменение" (например, добавление символа к строке) приводит к созданию нового объекта. В сценариях с интенсивной модификацией это создает нагрузку на сборщик мусора.
  2. Сложность создания сложных объектов: Для пошагового конструирования объекта со многими полями требуется использовать паттерн Builder или создавать множество промежуточных объектов.

Пример и практика создания на Java:

// 1. Класс объявлен как final, чтобы запретить наследование и переопределение методов.
public final class ImmutablePerson {
    // 2. Все поля объявлены как private и final.
    private final String name;
    private final int age;
    private final List<String> hobbies; // Изменяемый объект внутри!

    // 3. Данные устанавливаются только в конструкторе.
    public ImmutablePerson(String name, int age, List<String> hobbies) {
        this.name = name;
        this.age = age;
        // 4. КРИТИЧНО: Защитная копия (defensive copy) для изменяемых компонентов.
        this.hobbies = new ArrayList<>(hobbies);
    }

    // 5. Только геттеры, сеттеров нет.
    public String getName() { return name; }
    public int getAge() { return age; }
    // 6. Геттер для изменяемого поля также возвращает защитную копию.
    public List<String> getHobbies() {
        return new ArrayList<>(hobbies);
    }
}

// Использование Builder для удобного создания:
ImmutablePerson person = new ImmutablePerson.Builder()
        .setName("Alice")
        .setAge(30)
        .addHobby("Reading")
        .addHobby("Cycling")
        .build();