Вопрос 9 · Раздел 13

Что делать, если поле иммутабельного класса ссылается на мутабельный объект?

Если ваше поле ссылается на изменяемый объект (например, ArrayList, Date, массив), нужно сделать его копию.

Версии по языкам: English Russian Ukrainian

Junior Level

Если ваше поле ссылается на изменяемый объект (например, ArrayList, Date, массив), нужно сделать его копию.

Простой пример

public final class Person {
    private final String name;
    private final Date birthDate; // Date — мутабельный!

    public Person(String name, Date birthDate) {
        this.name = name;
        // Альтернатива: birthDate.clone() — для Date это работает, но clone()
        // в целом считается broken (Effective Java Item 13).
        this.birthDate = new Date(birthDate.getTime()); // КОПИЯ!
    }

    public Date getBirthDate() {
        return new Date(birthDate.getTime()); // КОПИЯ!
    }
}

Правило двух точек

  1. На входе (конструктор) — копируйте входящие мутабельные объекты
  2. На выходе (геттер) — возвращайте копии внутренних мутабельных объектов

Когда defensive copy избыточен

Если вы контролируете весь код, который передаёт данные, и это internal API — можно документировать контракт «caller must not mutate». Но будьте осторожны: будущие разработчики могут не прочитать документацию.


Middle Level

Защита коллекций

public final class ImmutableClass {
    private final List<String> items;

    public ImmutableClass(List<String> items) {
        // Плохо: this.items = items;
        // Хорошо:
        this.items = List.copyOf(items); // Java 10+
    }

    public List<String> getItems() {
        return items; // List.copyOf вернул иммутабельный список — можно возвращать
    }
}

Старые версии Java

// Java 8
this.items = Collections.unmodifiableList(new ArrayList<>(items));

// Или
this.items = new ArrayList<>(items);
// ...
return Collections.unmodifiableList(items);

Глубокое копирование

Если список содержит мутабельные объекты:

public record Order(Long id, List<Item> items) {
    public Order {
        items = items.stream()
            .map(item -> new Item(item.getName(), item.getPrice())) // копия каждого
            .toList();
    }
}

Современные альтернативы

  • Records — упрощают структуру, но коллекции копируются вручную
  • Guava ImmutableCollectionsImmutableList.copyOf()
  • Vavr — персистентные коллекции

Senior Level

TOCTOU-атака (Time-of-Check to Time-of-Use)

Если вы валидируете входящие данные, делайте копию до валидации:

public ImmutableClass(List<String> items) {
    List<String> copy = List.copyOf(items); // сначала копия
    if (copy.isEmpty()) throw new IllegalArgumentException(); // потом проверка
    this.items = copy;
}

Иначе в многопоточной среде состояние оригинального списка может измениться между проверкой и использованием.

Shallow vs Deep Copy — критическое различие

List.copyOf(users) создаёт новый список, но ссылки на те же объекты User. Изменив user.setName() во внешнем коде, вы измените данные внутри “иммутабельного” класса.

Решение: клонировать каждый элемент.

Производительность

Копирование больших коллекций — операция с линейной сложностью O(n), которая при больших n и частых вызовах создаёт значительный overhead. В “горячих” путях рассмотрите:

  • Персистентные структуры данных (Vavr)
  • Копирование только при изменении (Copy-On-Write)
  • Иммутабельные типы на уровне API

Резюме для Senior

  • Относитесь к любым входящим мутабельным объектам с недоверием
  • Всегда делайте копии массивов и стандартных коллекций
  • Различайте Shallow и Deep Copy
  • Копируйте перед валидацией
  • Если возможно, используйте List.of, Map.copyOf

🎯 Шпаргалка для интервью

Обязательно знать:

  • Правило двух точек: копировать на входе (конструктор) и на выходе (геттер)
  • Для Date: new Date(date.getTime()) — создаёт независимую копию
  • Для коллекций: List.copyOf(input) — Java 10+, создаёт иммутабельную копию
  • TOCTOU-атака: копируйте ПЕРЕД валидацией, иначе данные изменятся между проверкой и использованием
  • Shallow vs Deep Copy: List.copyOf(users) копирует контейнер, но не объекты User
  • Для глубокого копирования: stream().map(item -> new Item(item)).toList()
  • Когда defensive copy избыточен: полностью контролируемый internal API

Частые уточняющие вопросы:

  • Что копировать — на входе или на выходе? — И там, и там: входящие данные и возвращаемые из геттера
  • List.copyOf vs Collections.unmodifiableList? — List.copyOf делает копию; unmodifiableList — обёртка над оригиналом
  • Что делать с вложенными мутабельными объектами? — Deep Copy: клонировать каждый элемент
  • Когда можно НЕ копировать? — Internal API, caller guaranteed не мутировать

Красные флаги (НЕ говорить):

  • «Можно вернуть оригинал если getter private» — будущие разработчики могут не прочитать документацию
  • «unmodifiableList в конструкторе достаточно» — это обёртка, оригинал можно изменить
  • «clone() — нормальный способ копирования» — clone() считается broken
  • «Копирование всегда дорого» — для небольших коллекций overhead пренебрежим

Связанные темы:

  • [[8. Достаточно ли сделать все поля final для иммутабельности]]
  • [[10. Что такое defensive copy (защитная копия)]]
  • [[11. Когда нужно делать defensive copy]]
  • [[12. Как защитить коллекцию от изменений]]
  • [[14. В чём разница между shallow copy и deep copy]]