Полное руководство по глубокому копированию объектов Java
Узнайте, как создавать настоящие глубокие копии объектов Java, а не поверхностные ссылки. Изучите конструкторы копирования, интерфейс Cloneable и библиотеки.
Как создать настоящую копию объекта в Java, а не просто копию ссылки?
Рассмотрим следующий пример кода:
DummyBean dum = new DummyBean();
dum.setDummy("foo");
System.out.println(dum.getDummy()); // выводит 'foo'
DummyBean dumtwo = dum;
System.out.println(dumtwo.getDummy()); // выводит 'foo'
dum.setDummy("bar");
System.out.println(dumtwo.getDummy()); // выводит 'bar', но должно выводить 'foo'
При присваивании dumtwo = dum Java, кажется, копирует только ссылку, а не сам объект. Это означает, что изменение исходного объекта также влияет на «копию». Как можно создать полностью независимую копию объекта Java, чтобы изменения в оригинале не отражались на копии?
Какие существуют методы копирования объектов в Java и в каких случаях следует использовать каждый из них?
Создание истинной копии объекта в Java требует реализации одного из нескольких методов глубокого копирования, поскольку стандартное присваивание dumtwo = dum создаёт только ссылочный копию, указывающую на один и тот же объект в памяти. Чтобы получить независимую копию, в которой изменения исходного объекта не влияют на копию, можно использовать конструкторы копирования, реализовать интерфейс Cloneable, воспользоваться сериализацией/десериализацией или сторонними библиотеками, каждая из которых имеет свои преимущества и области применения в зависимости от структуры и требований вашего объекта.
Содержание
- Понимание проблемы: поверхностное vs глубокое копирование
- Метод 1: Конструкторы копирования
- Метод 2: Интерфейс Cloneable и метод clone()
- Метод 3: Сериализация и десериализация
- Метод 4: Сторонние библиотеки
- Метод 5: Ручные методы копирования
- Выбор подходящего метода
- [Лучшие практики и соображения](#лучшие-практики-и- соображения)
Понимание проблемы: поверхностное vs глубокое копирование
В Java, когда вы присваиваете один объект другому (dumtwo = dum), вы создаёте поверхностную копию – обе ссылки указывают на один и тот же объект в памяти. Это означает, что любые изменения, сделанные через одну ссылку, видны и через другую, как показано в вашем примере.
Глубокая копия создаёт полностью независимый объект со всеми его полями, скопированными рекурсивно. При изменении исходного объекта копия остаётся неизменной.
Ключевое различие: поверхностное копирование копирует только ссылки на объекты, тогда как глубокое копирование создаёт новые копии всех объектов в графе объектов.
Метод 1: Конструкторы копирования
Конструктор копирования – это конструктор, принимающий объект того же класса и создающий новый экземпляр с копированными значениями. Этот подход часто считается самым чистым и поддерживаемым способом реализации глубокого копирования.
Базовая реализация
public class DummyBean {
private String dummy;
// Обычный конструктор
public DummyBean() {
this.dummy = "";
}
// Конструктор копирования
public DummyBean(DummyBean other) {
this.dummy = other.dummy;
}
// Геттеры и сеттеры...
}
Использование
DummyBean dum = new DummyBean();
dum.setDummy("foo");
// Создаём истинную копию с помощью конструктора копирования
DummyBean dumtwo = new DummyBean(dum);
dum.setDummy("bar");
System.out.println(dumtwo.getDummy()); // выводит 'foo' (правильно!)
Глубокое копирование с вложенными объектами
public class Address {
private String city;
private String street;
// ...
}
public class Person {
private String name;
private Address address;
// Конструктор копирования с глубоким копированием
public Person(Person other) {
this.name = other.name;
this.address = new Address(other.address); // Создаём новый Address
}
}
Согласно Baeldung, «конструкторы копирования в Java не наследуются подклассами, что может быть как преимуществом, так и ограничением в зависимости от вашего случая использования».
Метод 2: Интерфейс Cloneable и метод clone()
Java предоставляет встроенный механизм clone() через интерфейс Cloneable. Однако этот подход имеет несколько ограничений и сложностей.
Базовая реализация
public class DummyBean implements Cloneable {
private String dummy;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
// Геттеры и сеттеры...
}
Использование
DummyBean dum = new DummyBean();
dum.setDummy("foo");
try {
DummyBean dumtwo = (DummyBean) dum.clone();
dum.setDummy("bar");
System.out.println(dumtwo.getDummy()); // выводит 'foo'
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
Важные соображения
- Метод
clone()создаёт поверхностную копию по умолчанию - Нужно вызывать
super.clone()и затем выполнять глубокое копирование изменяемых полей - Метод бросает
CloneNotSupportedException - Возвращаемый тип –
Object, требуется явное приведение - Финальные поля не могут быть клонированы
Как объясняет GeeksforGeeks, «если ваш класс в основном содержит изменяемые свойства, вам нужно определить конструктор копирования или переопределить метод clone для выполнения глубокого копирования».
Метод 3: Сериализация и десериализация
Этот подход использует API сериализации Java для создания глубоких копий, преобразуя объект в поток байтов, а затем обратно в новый объект.
Реализация
import java.io.*;
public class DeepCopyUtil {
@SuppressWarnings("unchecked")
public static <T extends Serializable> T deepCopy(T object) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(object);
oos.flush();
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (T) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("Failed to deep copy object", e);
}
}
}
Использование
// Класс должен реализовывать Serializable
public class DummyBean implements Serializable {
private String dummy;
// Геттеры и сеттеры...
}
// Создаём глубокую копию
DummyBean dum = new DummyBean();
dum.setDummy("foo");
DummyBean dumtwo = DeepCopyUtil.deepCopy(dum);
dum.setDummy("bar");
System.out.println(dumtwo.getDummy()); // выводит 'foo'
Требования и ограничения
- Все объекты в графе объектов должны реализовывать
Serializable - Переход по производительности из-за сериализации
- Может не работать корректно с не сериализуемыми объектами, такими как потоки, сокеты и т.д.
- Финальные поля могут не копироваться правильно
Согласно Baeldung, «мы можем сериализовать объект и затем десериализовать его в новый объект» для достижения глубокого копирования. LabEx отмечает, что этот метод «использует классы ByteArrayOutputStream, ObjectOutputStream, ByteArrayInputStream и ObjectInputStream для сериализации и десериализации объекта».
Метод 4: Сторонние библиотеки
Несколько библиотек предоставляют функциональность глубокого копирования с лучшей производительностью и меньшими ограничениями, чем стандартные подходы.
Библиотека XStream
import com.thoughtworks.xstream.XStream;
public class DeepCopyUtil {
private static final XStream xstream = new XStream();
public static <T> T deepCopy(T object) {
String xml = xstream.toXML(object);
return (T) xstream.fromXML(xml);
}
}
Библиотека Java Deep Cloning
import org.apache.commons.lang3.SerializationUtils;
public class DeepCopyUtil {
public static <T extends Serializable> T deepCopy(T object) {
return SerializationUtils.clone(object);
}
}
Преимущества сторонних библиотек
- Нет необходимости реализовывать
CloneableилиSerializable - Лучше производительность, чем сериализация
- Корректно обрабатывают циклические ссылки
- Более гибкие и поддерживаемые
Как упомянуто в Stack Overflow, «объекты не обязаны быть Serializable, и вы не используете рефлексию» с библиотеками вроде XStream.
Метод 5: Ручные методы копирования
Для полного контроля над процессом копирования можно реализовать собственные методы копирования.
Реализация
public class DummyBean {
private String dummy;
private List<String> items;
// Метод копирования
public DummyBean deepCopy() {
DummyBean copy = new DummyBean();
copy.dummy = this.dummy;
// Создаём новый список с теми же элементами
copy.items = new ArrayList<>(this.items);
return copy;
}
// Или статический метод
public static DummyBean deepCopy(DummyBean original) {
if (original == null) return null;
DummyBean copy = new DummyBean();
copy.dummy = original.dummy;
copy.items = new ArrayList<>(original.items);
return copy;
}
}
Использование
DummyBean dum = new DummyBean();
dum.setDummy("foo");
dum.getItems().add("item1");
// Создаём копию
DummyBean dumtwo = dum.deepCopy();
dum.setDummy("bar");
dum.getItems().add("item2");
// dumtwo остаётся неизменным
System.out.println(dumtwo.getDummy()); // выводит 'foo'
System.out.println(dumtwo.getItems()); // содержит только 'item1'
Выбор подходящего метода
Каждый метод имеет свои сильные и слабые стороны:
| Метод | Производительность | Сложность | Поддерживаемость | Лучшее применение |
|---|---|---|---|---|
| Конструктор копирования | Высокая | Низкая | Высокая | Простые объекты, новый код |
| Clone() | Средняя | Высокая | Средняя | Унаследованный код, критичность к производительности |
| Сериализация | Низкая | Средняя | Низкая | Сложные графы объектов, прототипирование |
| Сторонние | Средняя | Низкая | Высокая | Производственный код, сложные объекты |
| Ручной | Высокая | Высокая | Средняя | Полный контроль над копированием |
Согласно Stack Overflow, «если Graph имеет подкласс, например, добавляющий цвет к каждому узлу, вызов конструктора копирования в Graph позволит скопировать только базовый граф, потеряв дополнительную информацию и функциональность подклассов».
Лучшие практики и соображения
- Учитывайте изменяемость объектов: если ваши объекты неизменяемы (например,
String), поверхностное копирование достаточно. - Обрабатывайте циклические ссылки: методы на основе сериализации могут не работать с циклическими ссылками.
- Проблемы производительности: сериализация самая медленная, конструкторы копирования обычно самые быстрые.
- Обработка исключений: корректно обрабатывайте исключения в
clone()и методах сериализации. - Документируйте стратегию копирования: чётко документируйте, как объекты должны копироваться в вашем коде.
- Проверьте глубокие копии: убедитесь, что ваши методы копирования работают правильно с сложными графами объектов.
Как предлагает Java Practices, «при реализации конструкторов копирования учитывайте, нужна ли вам защитная копия для изменяемых полей».
Источники
- How to Make a Deep Copy of an Object in Java | Baeldung
- Copy Constructor in Java | Baeldung
- Java Object clone() Method - Cloning in Java | DigitalOcean
- Java Cloning - Deep and Shallow Copy - Copy Constructors | HowToDoInJava
- How to implement deep copying in Java | LabEx
- Shallow and deep copy: Two ways to copy objects in Java | InfoWorld
- Java Cloning: Copy Constructors vs. Cloning | DZone
- Faster Deep Copies of Java Objects – JavaTechniques
- Understanding Deep Copy in Java | Medium
- What are Copy Constructors in Java & Types (With Examples) | ScholarHat
Заключение
Создание истинных копий объектов в Java требует реализации одного из нескольких методов глубокого копирования в зависимости от ваших конкретных потребностей:
- Конструкторы копирования предлагают лучшее сочетание производительности, поддерживаемости и ясности для нового кода.
- Интерфейс Cloneable предоставляет встроенную поддержку, но сопряжён с сложностями и ограничениями.
- Сериализация/десериализация подходит для сложных графов объектов, но имеет накладные расходы по производительности.
- Сторонние библиотеки как XStream обеспечивают надёжные решения без ограничений.
- Ручные методы копирования дают полный контроль над процессом копирования.
Для большинства случаев рекомендуется использовать конструкторы копирования: они самые простые, поддерживаемые и быстрые, избегают сложностей с Cloneable, не требуют накладных расходов сериализации и ясно выражают намерения в коде.
При реализации любой стратегии копирования всегда учитывайте, нужен ли вам глубокий или поверхностный копии, корректно обрабатывайте изменяемые поля и тщательно тестируйте с вашими конкретными графами объектов, чтобы убедиться, что копии ведут себя так, как ожидается.