Клонирование объектов в Java
Копирование объектов в Java с помощью clone(), интерфейса Cloneable и разница между поверхностным и глубоким копированием.
Клонирование объекта означает создание нового объекта с таким же состоянием — копии, которую можно изменять независимо от оригинала. Встроенный механизм Java — это Object.clone() в сочетании с маркерным интерфейсом Cloneable, однако этот подход имеет достаточно шероховатостей, поэтому большинство современного кода предпочитает конструктор копирования или фабричный метод. В этой главе рассмотрены оба подхода и ловушка между ними.
Встроенный способ: Object.clone()
Object.clone() является protected и выполняет поверхностное копирование полей экземпляра. Чтобы воспользоваться им, необходимо:
- Реализовать в классе интерфейс
Cloneable— маркерный интерфейс без методов. Без негоclone()выбрасываетCloneNotSupportedException. - Переопределить
clone(), сделав егоpublicи (как правило) сузив возвращаемый тип до конкретного класса.
public class Box implements Cloneable {
int size;
@Override
public Box clone() {
try {
return (Box) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e); // can't happen — we implement Cloneable
}
}
}super.clone() создаёт фактическую копию. Блок try/catch — это формальность: проверяемое исключение объявлено в Object.clone, но поскольку наш класс реализует Cloneable, исключение недостижимо.
Поверхностное копирование: что происходит на самом деле
Поверхностная копия дублирует непосредственные поля. Ссылки внутри объекта копируются как ссылки, а не как новые объекты — поэтому оригинал и клон разделяют всё то, на что эти ссылки указывают:
public class Person implements Cloneable {
String name;
int[] scores;
@Override
public Person clone() {
try { return (Person) super.clone(); }
catch (CloneNotSupportedException e) { throw new AssertionError(e); }
}
}
Person a = new Person();
a.scores = new int[]{1, 2, 3};
Person b = a.clone();
b.scores[0] = 99;
System.out.println(a.scores[0]); // 99 — they share the same arrayДля примитивов и неизменяемых значений (String, Integer, LocalDate) поверхностного копирования достаточно. Для изменяемых вложенных объектов это почти всегда неверно — изменение клона затрагивает и оригинал.
Глубокое копирование: решение проблемы
Чтобы получить по-настоящему независимую копию, переопределите clone() с рекурсивным копированием изменяемых полей:
@Override
public Person clone() {
try {
Person copy = (Person) super.clone();
copy.scores = scores.clone(); // arrays have their own clone()
return copy;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}Массивы реализуют Cloneable нативно и копируются с помощью .clone(). Коллекции — нет: для поля типа List<String> следует написать copy.items = new ArrayList<>(items); (см. ArrayList). Если граф объектов содержит собственные изменяемые типы, каждый тип в этом графе должен участвовать в глубоком копировании.
Почему у Cloneable плохая репутация
Несколько особенностей делают clone() неудобным:
- Это маркерный интерфейс без метода
clone()— самCloneableничего не предоставляет, контракт находится на уровнеObject.clone(). - Он обходит конструкторы — поля нового объекта заполняются JVM, поэтому инварианты, установленные в конструкторе, не проверяются заново.
- Подклассы наследуют обязательство: если
Parentпереопределяетclone(), каждый подкласс должен поддерживать логику глубокого копирования в актуальном состоянии, иначе он молча наследует нерабочую поверхностную версию. - Проверяемое исключение, приведение типа, вызов
super.clone()— каждое переопределение повторяет один и тот же шаблонный код.
Современная альтернатива: конструктор копирования
Конструктор копирования — это обычный конструктор, принимающий экземпляр того же класса и копирующий его поля:
public class Person {
String name;
int[] scores;
public Person(Person other) {
this.name = other.name;
this.scores = other.scores.clone(); // deep where it matters
}
}
Person b = new Person(a);Он выполняется через обычный конструктор, поэтому инварианты проверяются. Это чистый Java-код без маркерных интерфейсов, CloneNotSupportedException и приведений типов. Подклассы просто пишут свой конструктор копирования, вызывающий super(other). Рекомендация Effective Java — предпочитать конструкторы копирования (или статические фабрики copyOf) вместо clone.
Классы, подобные коллекциям, уже следуют этому паттерну: new ArrayList<>(other), new HashMap<>(other), Set.copyOf(other).
Какой подход использовать?
| Ситуация | Рекомендуемый подход |
|---|---|
| Новый простой класс под вашим контролем | Конструктор копирования или статическая фабрика copyOf |
| Класс только с примитивными / неизменяемыми полями | Любой — даже поверхностный clone() безопасен |
| Класс с изменяемыми полями (списки, массивы, вложенные объекты) | Конструктор копирования с явным глубоким копированием |
Существующий API, уже требующий Cloneable | Переопределить clone() и глубоко скопировать изменяемые поля |
| Тип-значение, который можно перепроектировать | Сделать его неизменяемым — тогда копирование не понадобится вовсе |
Коротко: предпочитайте конструктор копирования для нового кода, а к clone() обращайтесь лишь тогда, когда этого требует существующий контракт.
Records и неизменяемые типы
Records являются неизменяемыми, поэтому они вообще не нуждаются в клонировании — одну и ту же ссылку можно передавать где угодно. Если нужна изменённая копия, напишите небольшие методы with...:
record Point(int x, int y) {
Point withX(int newX) { return new Point(newX, y); }
}Этот стиль — «создать новый экземпляр с одним изменённым полем» — обычно нагляднее, чем клонирование с последующей мутацией.
Практический пример
Что дальше
Большинство проблем с клонированием исчезает, если класс изначально является неизменяемым — нечего защитно копировать, никаких сюрпризов с алиасингом, безопасно использовать в нескольких потоках. В следующей главе рассматривается, как проектировать класс именно таким образом. Продолжите чтение: Неизменяемые классы в Java.