Питання 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. Коли потрібно робити захисну копію]]
  • [[12. Як захистити колекцію від змін]]
  • [[14. В чому різниця між shallow copy та deep copy]]