Когда нужно делать defensive copy
Защитная копия (Defensive Copy) обязательна во всех случаях, когда класс пересекает границы доверия и оперирует мутабельными (изменяемыми) объектами:
🟢 Junior Level
30-секундный ответ
Защитная копия (Defensive Copy) обязательна во всех случаях, когда класс пересекает границы доверия и оперирует мутабельными (изменяемыми) объектами:
- При приёме данных (Конструкторы и фабричные методы): если аргумент конструктора является изменяемым типом (массив
byte[], коллекцияList,Map,Dateили мутабельный доменный класс), необходимо снять независимую копию, чтобы внешний код не мог испортить состояние объекта после его создания. - При возврате данных (Геттеры и методы доступа): если внутреннее поле класса мутабельно, возвращать нужно копию или неизменяемое представление, чтобы клиентский код не мог изменить инварианты класса.
Защитная копия КАТЕГОРИЧЕСКИ НЕ НУЖНА для:
- Примитивных типов (
int,long,boolean,double). - Известных неизменяемых классов (
String,BigDecimal,BigInteger,UUID). - Современных классов даты и времени пакета
java.time(LocalDate,Instant,Duration).
Наглядный пример: Когда копия обязательна
public final class UserRegistration {
private final String username; // Неизменяем — копия НЕ нужна
private final byte[] passwordHash; // Массив мутабелен — копия ОБЯЗАТЕЛЬНА!
public UserRegistration(String username, byte[] passwordHash) {
this.username = username;
// Копия на входе: защищает от изменения массива вызывающим кодом
this.passwordHash = passwordHash.clone();
}
public String getUsername() {
return username; // Прямой возврат безопасен
}
public byte[] getPasswordHash() {
// Копия на выходе: защищает внутренний массив от перезаписи извне
return passwordHash.clone();
}
}
🟡 Middle Level
4 типовых сценария обязательного защитного копирования
1. Любые массивы (T[], byte[], int[])
Массивы в Java всегда изменяемы по индексам. Даже ссылка final byte[] не защищает элементы.
- В конструкторе:
this.data = data.clone(); - В геттере:
return data.clone();
2. Коллекции Java Collections Framework (List, Set, Map)
Стандартные коллекции (ArrayList, HashMap, HashSet) предоставляют методы мутации add(), remove(), clear().
- В конструкторе (Java 10+):
this.items = List.copyOf(items);(илиnew ArrayList<>(items)с обёрткойCollections.unmodifiableList). - Нюанс
null-безопасности:List.copyOf()выбрасываетNullPointerException, если переданный список содержитnull. Если элементыnullдопустимы бизнес-логикой, используютCollections.unmodifiableList(new ArrayList<>(items)).
3. Устаревшие классы даты (java.util.Date, java.util.Calendar)
Класс Date имеет сеттеры setTime(), setYear(). В legacy-коде требуется создание нового экземпляра: new Date(date.getTime()). В современном коде следует заменять Date на java.time.Instant или LocalDate, которые иммутабельны по дизайну.
4. Вложенные мутабельные доменные сущности
Если объект содержит вложенную сущность Address с сеттерами, защитное копирование списка адресов List.copyOf(addresses) создаст лишь поверхностную копию. Для защиты необходимо глубокое копирование:
this.addresses = addresses.stream()
.map(addr -> new Address(addr.getCity(), addr.getStreet()))
.toList();
Когда защитное копирование является антипаттерном
- Передача владения (Handover of Ownership): если объект конструируется через
Builder, и строитель гарантирует, что созданная коллекция больше никем не будет использоваться. - Внутренние изолированные структуры (Private Helpers): когда коллекция создаётся внутри приватного метода и никогда не отдаётся наружу.
- Объекты-переносчики данных (Mutable DTO): если класс изначально проектировался как изменяемый контейнер для десериализации Jackson/Hibernate.
🔴 Senior Level
Архитектурные границы доверия (Trust Boundaries)
Решение о выполнении защитного копирования принимается на основе анализа границ доверия архитектуры:
- Внешняя граница (Public API, библиотеки, открытые SDK, REST-контроллеры): доверие к клиенту равно нулю. Защитное копирование строго обязательно как на входе, так и на выходе.
- Внутренняя граница (Package-Private, модули с высокой связностью): в критичных к задержкам сервисах (HFT, Real-time telemetry) сплошное $O(N)$ копирование создаёт недопустимый GC Churn в Eden. В таких сценариях границы документируются контрактом («Caller transfers ownership and must not mutate»), либо используются специализированные неизменяемые типы на уровне сигнатур методов.
Проблема TOCTOU при защитном копировании
Если конструктор проверяет бизнес-правила (например, «список прав не должен быть пустым и не должен содержать дубликатов»), проверка должна выполняться строго после снятия защитной копии:
// ✅ ЗАЩИТА ОТ TOCTOU:
public SecurityContext(List<String> permissions) {
// 1. Сначала отсекаем внешние потоки локальной копией:
List<String> copy = List.copyOf(permissions);
// 2. Валидируем уже свою локальную неизменяемую копию:
if (copy.isEmpty() || !copy.contains("BASE_ACCESS")) {
throw new SecurityException("Invalid permissions");
}
this.permissions = copy;
}
Альтернативы $O(N)$ копированию: Zero-Copy и Persistent Data Structures
В системах с высокими требованиями к пропускной способности аллокация тысяч копий коллекций приводит к частым Minor GC паузам. Архитектурные альтернативы:
- Персистентные структуры данных (Vavr, PCollections): используют префиксные деревья (HAMT). Добавление элемента порождает новый узел за $O(\log_{32} N)$, переиспользуя до 95% существующей структуры в памяти (Structural Sharing).
- Zero-Copy Read-Only Views:
ByteBuffer.asReadOnlyBuffer()— накладывает запрет на запись в нативный буфер без копирования байтов.MemorySegment.asReadOnly()(Project Panama / JEP 454 Foreign Function & Memory API) — безопасный доступ к off-heap памяти без аллокаций в куче.
4 Tricky Questions
1. Должен ли метод build() в паттерне Builder выполнять защитное копирование внутренней коллекции перед передачей в конструктор иммутабельного класса?
Ответ:
Да, абсолютно обязательно!
Если Builder.build() передаст ссылку на свой внутренний список this.items напрямую в конструктор иммутабельного объекта без копирования, возникает опасная брешь:
OrderBuilder builder = new OrderBuilder().addItem(item1);
Order order = builder.build();
// Злоумышленник или невнимательный разработчик продолжает использовать builder:
builder.addItem(item2); // Если build() не сделал копию, order изменился!
Либо Builder должен сделать List.copyOf(this.items) при вызове build(), либо конструктор Order обязан сделать защитную копию. На практике безопаснее всего делать копию внутри конструктора целевого класса.
2. Как в Foreign Function & Memory API (Java 22, JEP 454) решается проблема защитного копирования нативной памяти без накладных расходов?
Ответ:
При работе с нативной памятью (off-heap) копирование мегабайтных буферов крайне неэффективно.
Вместо физического копирования памяти метод MemorySegment.asReadOnly() создаёт легковесную обёртку-дескриптор, переводя сегмент памяти в режим «только для чтения».
Любая попытка вызова нативных операций записи (segment.set(...)) вызовет немедленное исключение UnsupportedOperationException на уровне проверок рантайма HotSpot, гарантируя неизменяемость данных в нативной памяти без выделения дополнительной RAM (Zero-Copy Immutability).
3. Если входящая коллекция содержит 100 000 элементов, как избежать падения производительности из-за $O(N)$ защитного копирования?
Ответ: Применяются 3 архитектурных паттерна:
- Иммутабельные типы в контракте API: сигнатура метода проектируется так, чтобы принимать не абстрактный
Collection<T>, а конкретный неизменяемый тип (например,io.vavr.collection.List<T>или специализированный неизменяемый доменный тип). В этом случае ответственность за неизменяемость переносится на вызывающую сторону. - Передача владения через Builder: вызывающий код передаёт фабрику или Stream, а иммутабельный класс выполняет наполнение контейнера ровно один раз.
- Memory Mapped / Paged Views: если данные предназначены только для пакетной обработки, они маппятся в неизменяемые буферы памяти или оборачиваются в специализированные структуры со структурным разделением (Structural Sharing).
4. Почему вызов List.copyOf(list) работает за $O(1)$ без аллокаций, если список уже был создан через List.of()?
Ответ:
Метод List.copyOf() содержит оптимизацию:
if (coll instanceof ImmutableCollections.AbstractImmutableCollection) {
return (List<E>) coll;
}
Все списки, созданные через List.of(), Set.of(), Map.of() или предыдущие вызовы copyOf(), являются наследниками системного внутреннего класса AbstractImmutableCollection. JVM точно знает, что эти коллекции физически неизменяемы и не содержат null. Поэтому метод мгновенно возвращает переданную ссылку без валидации и без копирования элементов массива, обеспечивая нулевую стоимость вызова.
🎯 Шпаргалка для интервью
Матрица принятия решений: Делать ли Defensive Copy?
| Входные/Выходные данные | Делать Defensive Copy? | Обоснование |
| :— | :— | :— |
| int, double, boolean | ❌ Нет | Примитивы передаются по значению |
| String, UUID, BigDecimal | ❌ Нет | Классы уже неизменяемы по спецификации |
| LocalDate, Instant | ❌ Нет | Пакет java.time спроектирован иммутабельным |
| Массивы byte[], int[] | ✅ Да, всегда | Массивы всегда мутабельны по индексам (.clone()) |
| List, Set, Map | ✅ Да, всегда | Внешний код может вызвать .add() / .clear() (List.copyOf()) |
| Date, Calendar | ✅ Да, всегда | Содержат методы-мутаторы (new Date(time)) |
| Внутренний доверенный код | ⚠️ По соглашению | Если задокументирована передача владения ради высокой скорости |
Красные флаги на собеседовании (Чего говорить нельзя)
- ❌ «Защитную копию нужно делать вообще для любых объектов, включая String» — грубая ошибка, строки неизменяемы, их копирование бессмысленно.
- ❌ «В конструкторе можно просто обернуть входной список в Collections.unmodifiableList» — это не копия, а обёртка; изменение оригинала повредит класс.
- ❌ «Массив final int[] защищён от изменений» — элементы массива мутабельны.
- ❌ «Проверку аргументов на валидность нужно делать перед созданием защитной копии» — открывает уязвимость TOCTOU.