Что делать, если поле иммутабельного класса ссылается на мутабельный объект?
Если ваше поле ссылается на изменяемый объект (например, 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. Когда нужно делать defensive copy]]
- [[12. Как защитить коллекцию от изменений]]
- [[14. В чём разница между shallow copy и deep copy]]