Що таке захисна копія (defensive copy)?
Як ксерокопія документа: ви віддаєте копію, і якщо хтось на ній пише, ваш оригінал не страждає.
Junior Level
Захисна копія (Defensive Copy) — це створення копії даних, щоб захистити внутрішній об’єкт від змін ззовні.
Як ксерокопія документа: ви віддаєте копію, і якщо хтось на ній пише, ваш оригінал не страждає.
Простий приклад
public class MyClass {
private Date date;
public MyClass(Date date) {
// ПОГАНО: this.date = date; // зовнішня змінна може змінитися
// ДОБРЕ:
this.date = new Date(date.getTime()); // створили копію
}
public Date getDate() {
return new Date(date.getTime()); // повертаємо копію
}
}
Навіщо це потрібно
Без копії код, що викликає, може змінити ваші внутрішні дані через посилання:
Date d = new Date();
MyClass obj = new MyClass(d);
d.setYear(2000); // Змінили внутрішній об'єкт MyClass!
Коли НЕ робити defensive copy
- Незмінні типи (String, Integer, LocalDate) — копіювати безглуздо
- Повністю контрольований internal API — caller guaranteed не мутувати
- Performance-critical код — копіювання кожного виклику створює unacceptable overhead
Middle Level
Дві точки захисту
1. В конструкторі (вхід даних)
public class MyObject {
private final Date startDate;
private final List<String> items;
public MyObject(Date date, List<String> items) {
this.startDate = new Date(date.getTime());
this.items = new ArrayList<>(items);
}
}
2. В гетері (вихід даних)
public Date getStartDate() {
return new Date(startDate.getTime());
}
public List<String> getItems() {
return Collections.unmodifiableList(new ArrayList<>(items));
}
Shallow vs Deep Copy
- Shallow Copy — копіюється контейнер, але елементи залишаються тими самими
List<String> copy = new ArrayList<>(original); - Deep Copy — рекурсивно копіюються усі вкладені об’єкти
List<User> copy = original.stream() .map(u -> new User(u.getName(), u.getAge())) .toList();
Сучасний підхід (Java 10+)
this.items = List.copyOf(items); // незмінна копія
Senior Level
TOCTOU та порядок операцій
Копіюйте перед валідацією:
public SecureClass(List<String> roles) {
List<String> copy = new ArrayList<>(roles); // 1. Копія
if (copy.contains("admin")) { // 2. Перевірка
// ...
}
this.roles = List.copyOf(copy); // 3. Збереження
}
Інакше можливий TOCTOU-attack: дані зміняться між перевіркою та використанням.
Wrapper vs Copy
Collections.unmodifiableList(original) — це обгортка, не копія. Якщо original зміниться, обгортка теж зміниться. Використовуйте лише для повернення з гетера, але не в конструкторі.
Продуктивність
Захисне копіювання — операція $O(n)$. У високонавантажених системах:
- Уникайте копіювання в гарячих циклах
- Використовуйте персистентні структури (Vavr) для структурного розділення
- Кешувати копії можна лише якщо дані дійсно статичні; інакше ризикуєте повернути застарілі дані.
Резюме для Senior
- Defensive copy — створення “знімка” змінних даних
- Обов’язкова умова для незмінності за наявності полів-об’єктів
- Завжди робіть копію перед валідацією
Collections.unmodifiableList()— обгортка, а не копія; використовуйтеList.copyOf()для справжнього захисту
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- Defensive Copy — створення копії даних для захисту від змін ззовні
- Дві точки захисту: конструктор (вхід) та гетер (вихід)
- Shallow Copy — копіюється контейнер, Deep Copy — рекурсивно усі вкладені об’єкти
- TOCTOU: копіюйте ПЕРЕД валідацією, інакше дані зміняться між перевіркою та використанням
- Wrapper vs Copy:
unmodifiableList— обгортка (O(1)),List.copyOf— копія (O(n)) - Коли НЕ робити: незмінні типи, internal API, performance-critical код
Часті уточнюючі запитання:
- Що дешевше — обгортка чи копія? — Обгортка O(1), копія O(n); але обгортка не захищає від змін оригіналу
- Коли shallow copy достатньо? — Коли колекція містить незмінні об’єкти (String, Integer)
- Що таке TOCTOU-атака? — Дані змінюються між перевіркою та копіюванням
List.copyOfvsnew ArrayList? — List.copyOf повертає незмінний список; new ArrayList — змінний
Червоні прапорці (НЕ говорити):
- «UnmodifiableList — це копія» — це обгортка, зміни оригіналу видимі
- «Defensive copy завжди потрібен» — для незмінних типів безглуздий
- «clone() — надійний спосіб» — clone() вважається broken (Effective Java Item 13)
- «Копіювання після валідації — правильно» — потрібно КОПІЮВАТИ ПЕРЕД валідацією
Пов’язані теми:
- [[8. Чи достатньо зробити всі поля final для незмінності]]
- [[9. Що робити, якщо поле класу посилається на змінний об’єкт]]
- [[11. Коли потрібно робити захисну копію]]
- [[12. Як захистити колекцію від змін]]
- [[14. В чому різниця між shallow copy та deep copy]]