Avatar ·

Как правильно скопировать объект в Java: поверхностное и глубокое копирование

Всем привет! Я только начал изучать Java и столкнулся с проблемой, которая меня очень смущает. Я пытаюсь скопировать объект, но получаю неожиданные результаты. Допустим, у меня есть простой класс Person с полями name и age, а также вложенным объектом Address. Когда я пишу: ```java Person p1 = new Person("Иван", 30, new Address("Москва", "Тверская", 10)); Person p2 = p1; p2.setName("Петр"); System.out.println(p1.getName()); // Выводит "Петр" - почему?! ``` Я ожидал, что p1 останется "Иваном", а p2 станет "Петром". Но, как я понял, это просто копирование ссылки. Я попробовал использовать метод clone(): ```java public class Person implements Cloneable { private String name; private Address address; // конструктор, геттеры, сеттеры @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } } ``` Но когда я клонировал и менял адрес у копии, у оригинала адрес тоже изменился. Почитал, что это называется "поверхностное копирование". Как сделать так, чтобы при копировании создавалась полностью независимая копия объекта со всеми вложенными объектами? И что вообще считается правильным способом копирования объектов в Java? Может, есть какие-то best practices или библиотеки для этого? <<>>

Проблема копирования ссылок и поверхностного копирования

Вы абсолютно правы: операция 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).