Другое

Полное руководство по глубокому копированию объектов Java

Узнайте, как создавать настоящие глубокие копии объектов Java, а не поверхностные ссылки. Изучите конструкторы копирования, интерфейс Cloneable и библиотеки.

Как создать настоящую копию объекта в Java, а не просто копию ссылки?

Рассмотрим следующий пример кода:

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 глубокое копирование

В Java, когда вы присваиваете один объект другому (dumtwo = dum), вы создаёте поверхностную копию – обе ссылки указывают на один и тот же объект в памяти. Это означает, что любые изменения, сделанные через одну ссылку, видны и через другую, как показано в вашем примере.

Глубокая копия создаёт полностью независимый объект со всеми его полями, скопированными рекурсивно. При изменении исходного объекта копия остаётся неизменной.

Ключевое различие: поверхностное копирование копирует только ссылки на объекты, тогда как глубокое копирование создаёт новые копии всех объектов в графе объектов.

Метод 1: Конструкторы копирования

Конструктор копирования – это конструктор, принимающий объект того же класса и создающий новый экземпляр с копированными значениями. Этот подход часто считается самым чистым и поддерживаемым способом реализации глубокого копирования.

Базовая реализация

java
public class DummyBean {
    private String dummy;
    
    // Обычный конструктор
    public DummyBean() {
        this.dummy = "";
    }
    
    // Конструктор копирования
    public DummyBean(DummyBean other) {
        this.dummy = other.dummy;
    }
    
    // Геттеры и сеттеры...
}

Использование

java
DummyBean dum = new DummyBean();
dum.setDummy("foo");

// Создаём истинную копию с помощью конструктора копирования
DummyBean dumtwo = new DummyBean(dum);

dum.setDummy("bar");
System.out.println(dumtwo.getDummy()); // выводит 'foo' (правильно!)

Глубокое копирование с вложенными объектами

java
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. Однако этот подход имеет несколько ограничений и сложностей.

Базовая реализация

java
public class DummyBean implements Cloneable {
    private String dummy;
    
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    
    // Геттеры и сеттеры...
}

Использование

java
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 для создания глубоких копий, преобразуя объект в поток байтов, а затем обратно в новый объект.

Реализация

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);
        }
    }
}

Использование

java
// Класс должен реализовывать 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

java
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

java
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: Ручные методы копирования

Для полного контроля над процессом копирования можно реализовать собственные методы копирования.

Реализация

java
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;
    }
}

Использование

java
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 позволит скопировать только базовый граф, потеряв дополнительную информацию и функциональность подклассов».

Лучшие практики и соображения

  1. Учитывайте изменяемость объектов: если ваши объекты неизменяемы (например, String), поверхностное копирование достаточно.
  2. Обрабатывайте циклические ссылки: методы на основе сериализации могут не работать с циклическими ссылками.
  3. Проблемы производительности: сериализация самая медленная, конструкторы копирования обычно самые быстрые.
  4. Обработка исключений: корректно обрабатывайте исключения в clone() и методах сериализации.
  5. Документируйте стратегию копирования: чётко документируйте, как объекты должны копироваться в вашем коде.
  6. Проверьте глубокие копии: убедитесь, что ваши методы копирования работают правильно с сложными графами объектов.

Как предлагает Java Practices, «при реализации конструкторов копирования учитывайте, нужна ли вам защитная копия для изменяемых полей».

Источники

  1. How to Make a Deep Copy of an Object in Java | Baeldung
  2. Copy Constructor in Java | Baeldung
  3. Java Object clone() Method - Cloning in Java | DigitalOcean
  4. Java Cloning - Deep and Shallow Copy - Copy Constructors | HowToDoInJava
  5. How to implement deep copying in Java | LabEx
  6. Shallow and deep copy: Two ways to copy objects in Java | InfoWorld
  7. Java Cloning: Copy Constructors vs. Cloning | DZone
  8. Faster Deep Copies of Java Objects – JavaTechniques
  9. Understanding Deep Copy in Java | Medium
  10. What are Copy Constructors in Java & Types (With Examples) | ScholarHat

Заключение

Создание истинных копий объектов в Java требует реализации одного из нескольких методов глубокого копирования в зависимости от ваших конкретных потребностей:

  • Конструкторы копирования предлагают лучшее сочетание производительности, поддерживаемости и ясности для нового кода.
  • Интерфейс Cloneable предоставляет встроенную поддержку, но сопряжён с сложностями и ограничениями.
  • Сериализация/десериализация подходит для сложных графов объектов, но имеет накладные расходы по производительности.
  • Сторонние библиотеки как XStream обеспечивают надёжные решения без ограничений.
  • Ручные методы копирования дают полный контроль над процессом копирования.

Для большинства случаев рекомендуется использовать конструкторы копирования: они самые простые, поддерживаемые и быстрые, избегают сложностей с Cloneable, не требуют накладных расходов сериализации и ясно выражают намерения в коде.

При реализации любой стратегии копирования всегда учитывайте, нужен ли вам глубокий или поверхностный копии, корректно обрабатывайте изменяемые поля и тщательно тестируйте с вашими конкретными графами объектов, чтобы убедиться, что копии ведут себя так, как ожидается.

Авторы
Проверено модерацией
Модерация