Что такое defensive copy (защитная копия)
Главное назначение защитной копии — гарантировать целостность и неизменяемость объекта независимо от действий вызывающего кода. Паттерн реализуется по «правилу двух точек»:
🟢 Junior Level
30-секундный ответ
Защитная копия (Defensive Copy) — это идиома программирования, при которой создаётся независимый дубликат мутабельного объекта для изоляции внутреннего состояния класса от внешних изменений.
Главное назначение защитной копии — гарантировать целостность и неизменяемость объекта независимо от действий вызывающего кода. Паттерн реализуется по «правилу двух точек»:
- Точка входа (Конструктор): создаётся копия переданных параметров, чтобы предотвратить модификацию внутреннего состояния через исходные ссылки вызывающей стороны.
- Точка выхода (Геттер): возвращается копия внутреннего объекта (или неизменяемое представление), чтобы клиент не мог разрушить инварианты класса через полученную ссылку.
Аналогия из реальной жизни
Представьте нотариально заверенную копию паспорта:
- Если клиент просит ваш паспорт для оформления договора, вы отдаёте ксерокопию.
- Если клиент поставит на копии штамп или сделает пометку ручкой, ваш оригинальный паспорт в кармане останется целым и невредимым.
Наглядный пример
import java.util.Date;
public final class Appointment {
private final String title;
private final Date time; // java.util.Date мутабелен!
public Appointment(String title, Date time) {
this.title = title;
// ТОЧКА 1: Защитная копия на входе
this.time = new Date(time.getTime());
}
public String getTitle() {
return title;
}
public Date getTime() {
// ТОЧКА 2: Защитная копия на выходе
return new Date(time.getTime());
}
}
🟡 Middle Level
1. Поверхностная (Shallow) vs Глубокая (Deep) защитная копия
При создании защитных копий коллекций критически важно учитывать тип элементов:
- Shallow Copy (Поверхностная): создаётся новый контейнер, но ссылки внутри указывают на те же экземпляры. Достаточна, если сами элементы неизменяемы (
String,Integer,UUID):// Достаточно, так как String иммутабелен this.tags = List.copyOf(tags); - Deep Copy (Глубокая): рекурсивно копируется как сам контейнер, так и каждый вложенный изменяемый объект. Обязательна, если элементы мутабельны:
// Глубокое копирование мутабельных элементов через конструктор копирования: this.items = items.stream() .map(item -> new OrderItem(item.getSku(), item.getPrice())) .toList();
2. Защитная копия vs Неизменяемая обёртка (Wrapper)
Частая путаница на собеседованиях: Collections.unmodifiableList() vs List.copyOf():
Collections.unmodifiableList(original)— это обёртка (View) с $O(1)$ по времени и памяти. Она запрещает вызовadd()/remove()через себя, но не защищает от мутаций оригинального списка. Если внешний код изменитoriginal, изменения мгновенно отразятся в обёртке!List.copyOf(original)(Java 10+) — это полноценная защитная копия со сложностью $O(N)$. Она физически отсекает связь с оригиналом и возвращает неизменяемую коллекцию платформы.
List<String> original = new ArrayList<>(List.of("A", "B"));
List<String> view = Collections.unmodifiableList(original);
List<String> copy = List.copyOf(original);
original.add("C");
System.out.println(view); // [A, B, C] — ОБЁРТКА ИЗМЕНИЛАСЬ!
System.out.println(copy); // [A, B] — ЗАЩИТНАЯ КОПИЯ СОХРАНИЛА СОСТОЯНИЕ!
3. Когда защитная копия НЕ нужна
- Иммутабельные типы: примитивы,
String,BigDecimal, классы пакетаjava.time(LocalDate,Instant). - Закрытый доверенный код (Private/Package-Private API): когда класс и вызывающий код находятся внутри одного пакета и гарантируют отсутствие мутаций ради экстремальной производительности.
🔴 Senior Level
Защита от TOCTOU-атак (Time-of-Check to Time-of-Use)
В многопоточной среде валидация аргументов без предварительного копирования открывает критическую уязвимость:
// ❌ УЯЗВИМАЯ РЕАЛИЗАЦИЯ:
public TimeInterval(Date start, Date end) {
if (start.compareTo(end) > 0) { // Time of Check
throw new IllegalArgumentException();
}
// ОКНО УЯЗВИМОСТИ: Другой поток может вызвать start.setTime(Long.MAX_VALUE) прямо здесь!
this.start = new Date(start.getTime()); // Time of Use
this.end = new Date(end.getTime());
}
// ✅ ПРАВИЛЬНАЯ РЕАЛИЗАЦИЯ: Копия ДО валидации!
public TimeInterval(Date start, Date end) {
// 1. Сначала копируем во внутренние/локальные переменные:
Date localStart = new Date(start.getTime());
Date localEnd = new Date(end.getTime());
// 2. Валидируем изолированную копию:
if (localStart.compareTo(localEnd) > 0) {
throw new IllegalArgumentException();
}
this.start = localStart;
this.end = localEnd;
}
Гарантии JMM §17.5 при создании защитных копий
Если защитная копия создаётся внутри конструктора и сохраняется в final-поле:
- Все объекты, аллоцированные в процессе создания защитной копии (например, внутренние узлы
List.copyOfили новый массивbyte[]), считаются частью инициализации объекта. - Действие Freeze Action в конце конструктора устанавливает барьер
StoreStore, гарантируя, что поля защитной копии сброшены в память до публикации ссылки на основной объект. - Читающие потоки гарантированно видят консистентное состояние защитной копии без дополнительных блокировок.
Устранение аллокаций JIT-компилятором (Escape Analysis)
Senior-разработчик должен понимать издержки $O(N)$ копирования в горячих путях:
- При вызове геттера, возвращающего
new Date(time.getTime())илиarray.clone(), JVM создаёт новый объект в Eden. - Однако если вызывающий код использует результат локально:
long epoch = appointment.getTime().getTime(); - Высокоуровневый JIT-компилятор C2 выполняет Escape Analysis (Анализ побега): обнаружив, что возвращённая защитная копия не покидает стек текущего метода, C2 производит Scalar Replacement (Скалярное замещение), полностью исключая аллокацию объекта
Dateв куче!
4 Tricky Questions
1. В каком сценарии возврат Collections.unmodifiableList(this.list) из геттера на 100% безопасен и не требует выделения памяти на копирование?
Ответ:
Этот подход 100% безопасен, если внутренний список this.list был надёжно изолирован в конструкторе (через защитное копирование new ArrayList<>(input)), ссылка на него сохранена в private final поле, ни один метод класса никогда его не модифицирует, а обёртка Collections.unmodifiableList(this.list) создана один раз в конструкторе и сохранена во второе поле private final List<T> unmodifiableView;.
В этом случае геттер просто возвращает ссылку на предсозданный unmodifiableView за $O(1)$ без аллокаций и без риска изменения данных.
2. Защищает ли List.copyOf(input) в конструкторе от вредоносной реализации List, переданной злоумышленником?
Ответ:
Да, метод List.copyOf() спроектирован специально для защиты от атак через недоверенные реализации интерфейса List:
List.copyOf()проверяет класс аргумента: если это не внутренний доверенныйImmutableCollections, метод запрашивает массив элементов через стандартный итератор или методtoArray().- Внутренний массив элементов немедленно упаковывается в закрытый неизменяемый системный класс (
List12,ListN), запрещающий добавлениеnullи любых модификаций. - Любые переопределённые вредоносные методы переданного внешнего списка отсекаются, обеспечивая полную безопасность инвариантов.
3. Почему для полей-массивов (byte[], int[]) вызов array.clone() обязателен, даже если поле объявлено как private final?
Ответ:
Модификатор final запрещает лишь переприсвоение ссылки на сам массив. Сам массив в Java является мутабельным объектом фиксированной длины: любой клиент, имеющий ссылку на массив, может выполнить array[0] = 999;.
Поскольку у массивов нет геттеров и сеттеров, а доступ к элементам осуществляется напрямую по индексу, единственным способом защитить массив является вызов .clone() или Arrays.copyOf() как при приёме массива в конструкторе, так и при возврате из геттера.
4. Может ли C2 JIT-компилятор устранить затраты на защитную копию, возвращаемую из геттера? При каком условии?
Ответ:
Да, через механизм Escape Analysis и Scalar Replacement:
Если метод-геттер инлайнится (inlined) в вызывающий контекст, и скомпилированный код видит, что возвращённая копия (например, массив или объект Date) не передаётся во внешние методы, не сохраняется в статические/экземплярные поля и не используется другими потоками, C2 компилятор:
- Полностью отменяет физическую аллокацию нового объекта в куче (Heap).
- Заменяет поля объекта локальными регистрами процессора или стековыми переменными.
- Читает данные напрямую из памяти оригинального объекта, сводя оверхед защитной копии практически к нулю.
🎯 Шпаргалка для интервью
Сводная таблица подходов к Defensive Copy
| Тип объекта | В конструкторе (Вход) | В геттере (Выход) | Сложность |
| :— | :— | :— | :— |
| Коллекция (Immutable элементы) | List.copyOf(input) | Прямой возврат this.list | $O(N)$ вход / $O(1)$ выход |
| Коллекция (Mutable элементы) | stream().map(Item::new).toList() | Возврат списка клонированных копий | $O(N)$ глубокое копирование |
| Массивы (byte[]) | input.clone() | this.array.clone() | $O(N)$ вход / $O(N)$ выход |
| Устаревший Date | new Date(input.getTime()) | new Date(this.date.getTime()) | $O(1)$ аллокация |
| Современный Instant | Прямое присвоение (иммутабелен) | Прямой возврат | $O(1)$ без аллокаций |
Красные флаги на собеседовании (Чего говорить нельзя)
- ❌ «Collections.unmodifiableList() создаёт защитную копию списка» — это обёртка, при изменении оригинала данные в ней меняются.
- ❌ «Проверку аргументов на null и валидность нужно делать ДО создания защитной копии» — открывает уязвимость TOCTOU.
- ❌ «Для иммутабельных типов вроде String и LocalDate тоже нужно делать defensive copy» — бессмысленная трата ресурсов, иммутабельные типы копировать не нужно.
- ❌ «Если поле объявлено как final byte[], его элементы нельзя изменить» — элементы массива можно свободно перезаписывать по индексу.