Как сделать объект иммутабельным в Java?

«Как сделать объект иммутабельным в Java?» — вопрос из категории ООП, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Чтобы сделать объект иммутабельным (неизменяемым), необходимо следовать набору строгих правил при проектировании его класса. Это гарантирует, что состояние объекта останется постоянным после создания.

Пошаговый алгоритм:

  1. final класс: Объявите класс с модификатором final, чтобы предотвратить создание подклассов, которые могут нарушить иммутабельность.
  2. private final поля: Все поля должны быть объявлены как private и final. Это обеспечивает:
    • Инкапсуляцию (прямой доступ запрещен).
    • Однократную инициализацию (значение присваивается только в конструкторе).
  3. Отсутствие сеттеров: Не предоставляйте методов, изменяющих состояние полей.
  4. Безопасная инициализация в конструкторе:
    • Для примитивов и immutable-типов (например, String, Integer) — присваивайте напрямую.
    • Для mutable-объектов и коллекций — создавайте защитные копии (defensive copy).
  5. Безопасное возвращение значений из геттеров:
    • Для mutable-полей возвращайте глубокую копию объекта.
    • Для коллекций возвращайте неизменяемое представление (например, через Collections.unmodifiableList()) или копию.

Пример с коллекцией:

import java.util.*;

public final class ImmutableTeam {
    private final String name;
    private final List<String> members;

    public ImmutableTeam(String name, List<String> members) {
        this.name = name;
        // 1. Защитная копия при инициализации
        this.members = new ArrayList<>(members);
    }

    public String getName() { return name; }

    public List<String> getMembers() {
        // 2. Возвращаем неизменяемое представление внутренней коллекции
        return Collections.unmodifiableList(members);
    }
}
// Использование
List<String> originalList = new ArrayList<>(Arrays.asList("Alice", "Bob"));
ImmutableTeam team = new ImmutableTeam("Devs", originalList);
originalList.add("Charlie"); // Не влияет на team.members
team.getMembers().add("David"); // Выбросит UnsupportedOperationException

Соблюдение этих правил делает объект потокобезопасным по умолчанию и упрощает рассуждения о коде.