Неизменяемые классы и данные

Объект неизменяем, если после успешного конструктора его наблюдаемое состояние нельзя изменить. Операция «изменения» возвращает новый объект. Примеры в JDK: String, классы-обёртки, большинство типов java.time.

final у ссылки запрещает только повторное присваивание:

final List<String> names = new ArrayList<>();
names.add("Ada");              // допустимо: сам список изменяем
// names = new ArrayList<>();  // ошибка компиляции

Как спроектировать класс

  1. Не публиковать методов, меняющих состояние.
  2. Делать поля private final.
  3. Не позволять наследнику добавить изменяемое поведение: final-класс, record либо контролируемые конструкторы.
  4. Проверять инварианты в конструкторе.
  5. Делать защитные копии изменяемых аргументов и результатов.
  6. Не выпускать this из конструктора.
public final class Route {
    private final String id;
    private final List<String> points;
 
    public Route(String id, List<String> points) {
        this.id = Objects.requireNonNull(id);
        this.points = List.copyOf(points); // снимок структуры списка
    }
 
    public String id() {
        return id;
    }
 
    public List<String> points() {
        return points; // список уже неизменяем
    }
 
    public Route append(String point) {
        var copy = new ArrayList<>(points);
        copy.add(point);
        return new Route(id, copy);
    }
}

List.copyOf не делает глубокую копию элементов. Если элемент изменяем, его тоже надо копировать или заменить неизменяемым типом. Массивы и Date требуют особенно внимательной защитной копии.

Зачем

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

Цена — дополнительные объекты при частых изменениях. Для больших структур полезны builders, структурное разделение данных или локальная мутабельность внутри операции.

На собеседовании

Делает ли final класс неизменяемым? Нет. Он запрещает наследование, но поля могут оставаться изменяемыми.

Можно ли вернуть внутренний список? Да, только если он сам неизменяем и его элементы не дают изменить состояние владельца.

Почему immutable-объект удобен как ключ карты? Его equals и hashCode не меняются после помещения в карту.

Является ли record автоматически глубоко неизменяемым? Нет. Компоненты финальны, но могут ссылаться на изменяемые объекты.

Связано: final java, Инкапсуляция, Equals и hashCode.