Как правильно скопировать объект в Java: поверхностное и глубокое копирование
Проблема копирования ссылок и поверхностного копирования
Вы абсолютно правы: операция Person p2 = p1; не создает новый объект, а лишь копирует ссылку. Обе переменные указывают на один и тот же объект в куче, поэтому изменения через одну ссылку видны через другую. Это фундаментальное отличие Java от языков с явным управлением памятью.
Метод clone() из интерфейса Cloneable выполняет поверхностное копирование (shallow copy). Он создает новый объект, но его поля-ссылки продолжают указывать на те же самые вложенные объекты. Вот почему изменение адреса у копии меняет адрес у оригинала.
Три правильных способа копирования объектов
1. Глубокое копирование через конструктор копирования (Copy Constructor)
Это самый надежный и читаемый способ. Вы создаете конструктор, который принимает объект того же класса и инициализирует все поля, включая рекурсивное копирование вложенных объектов.
public class Address {
private String city;
private String street;
private int houseNumber;
// Конструктор копирования
public Address(Address other) {
this.city = other.city;
this.street = other.street;
this.houseNumber = other.houseNumber;
}
// остальные конструкторы и методы...
}
public class Person implements Cloneable {
private String name;
private int age;
private Address address;
// Конструктор копирования
public Person(Person other) {
this.name = other.name;
this.age = other.age;
// ВАЖНО: создаем новый объект Address на основе существующего
this.address = new Address(other.address);
}
// Обычный конструктор
public Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
public static void main(String[] args) {
Address addr = new Address("Москва", "Тверская", 10);
Person original = new Person("Иван", 30, addr);
Person copy = new Person(original); // Глубокая копия
copy.getAddress().setCity("Санкт-Петербург");
System.out.println(original.getAddress().getCity()); // "Москва" - оригинал не изменился
}
}
2. Статический фабричный метод copyOf()
Альтернатива конструктору копирования — статический метод, возвращающий новый экземпляр:
public class Person {
// поля, конструкторы...
public static Person copyOf(Person original) {
return new Person(original.name, original.age,
new Address(original.address));
}
}
// Использование:
Person copy = Person.copyOf(original);
3. Переопределенный метод clone() для глубокого копирования
Если вы настаиваете на clone(), переопределите его для глубокого копирования:
@Override
public Person clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
// Глубокое копирование вложенного объекта
cloned.address = new Address(this.address); // используем конструктор копирования Address
return cloned;
}
Важные рекомендации
- Не используйте clone() без крайней необходимости. Этот механизм считается "сломанным" в Java из-за сложности правильной реализации и отсутствия гарантий. Вместо этого используйте конструкторы копирования или фабричные методы.
- Для неизменяемых объектов (например, String, Integer, LocalDate) копирование не нужно — они безопасно разделяются между объектами.
- Для коллекций используйте
new ArrayList<>(originalList)илиoriginalList.stream().map(Item::new).collect(Collectors.toList())для глубокого копирования элементов. - Библиотеки-помощники: Apache Commons Lang3 (метод
SerializationUtils.clone()), библиотекаcloningот Google, или Java-модульjava.base(для простых случаев).
Когда какое копирование использовать?
- Поверхностное копирование — когда объекты неизменяемы (Immutable) или когда вы намеренно хотите разделить состояние (например, для кэширования).
- Глубокое копирование — когда копия должна быть полностью независимой (например, при передаче данных между слоями приложения, в многопоточности).
Таким образом, для вашего примера с Person и Address идеально подходит конструктор копирования. Он явный, типобезопасный и не требует дополнительных зависимостей.
<<Альтернативный подход: Сериализация для глубокого копирования
Если ваш класс и все его вложенные объекты реализуют интерфейс Serializable, вы можете использовать сериализацию для создания глубокой копии. Этот подход автоматически обрабатывает граф объектов любой сложности, включая циклические ссылки.
import java.io.*;
public class DeepCopyUtils {
@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);
}
}
}
// Использование:
Person copy = DeepCopyUtils.deepCopy(original);
Плюсы и минусы
- Плюсы: Работает для любых графов объектов, не требует написания конструкторов копирования для каждого класса. Отлично подходит для DTO (Data Transfer Objects).
- Минусы: Медленнее ручного копирования (в 10-100 раз). Требует, чтобы все классы в иерархии реализовали
Serializable. Может вызвать проблемы с transient-полями и статическими полями. Не подходит для объектов с ресурсами (соединения с БД, файловые дескрипторы).
Этот метод удобен для прототипирования или когда у вас сложная иерархия объектов, но для production-кода предпочтительнее ручное глубокое копирование или специализированные библиотеки (например, Cloner от Google).