Що робити, якщо поле незмінного класу посилається на змінний об'єкт?
Якщо ваше поле посилається на об'єкт, що змінюється (наприклад, ArrayList, Date, масив), потрібно зробити його копію.
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()); // КОПІЯ!
}
}
Правило двох точок
- На вході (конструктор) — копіюйте вхідні змінні об’єкти
- На виході (гетер) — повертайте копії внутрішніх змінних об’єктів
Коли 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 ImmutableCollections —
ImmutableList.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]]