Як зробити глибоку копію предмета?


301

Трохи складно реалізувати функцію копіювання глибокого об'єкта. Які кроки ви робите, щоб забезпечити вихідний об'єкт і клоновану поділитися без посилання?


4
Kryo має вбудовану підтримку копіювання / клонування . Це пряма копія з об'єкта в об'єкт, а не об'єкт-> байти-> об'єкт.
NateS

1
Ось відповідне питання, яке було задано пізніше: Рекомендація за корисні послуги глибокого клонування
Бред Купіт

Використання бібліотеки клонування врятувало мені день! github.com/kostaskougios/cloning
gaurav

Відповіді:


168

Безпечним способом є серіалізація об’єкта, а потім десеріалізація. Це забезпечує все абсолютно нове посилання.

Ось стаття про те, як це зробити ефективно.

Caveats: Класи можуть переосмислити серіалізацію таким чином, щоб нові екземпляри не створювалися, наприклад для одиночних клавіш. Крім того, це, звичайно, не працює, якщо ваші заняття не є серіалізаційними.


6
Майте на увазі, що реалізація FastByteArrayOutputStream, наведена у статті, може бути ефективнішою. Він використовує розширення в стилі ArrayList, коли буфер заповнюється, але краще використовувати підхід розширення в стилі LinkedList. Замість того, щоб створювати новий 2x буфер і заповнювати поточний буфер, підтримуйте пов'язаний список буферів, додаючи новий, коли поточний заповнюється. Якщо ви отримаєте запит на записування більшої кількості даних, ніж помістилось у вашому розмірі буфера за замовчуванням, створіть буферний вузол, який точно такий же, як і запит; вузли не повинні бути однакового розміру.
Брайан Харріс


Хороша стаття, яка пояснює глибоку копію через серіалізацію: javaworld.com/article/2077578/learn-java/…
Ad Infinitum

Зв'язаний список @BrianHarris не є більш ефективним, ніж динамічний масив. Вставлення елементів у динамічний масив амортизується постійною складністю, тоді як вставлення у зв’язаний список має лінійну складність
Norill Tempest

Наскільки серіалізація та десеріалізація повільніше, ніж підхід конструктора копій?
Воланд

75

Кілька людей згадували про використання або переосмислення Object.clone(). Не робіть цього. Object.clone()має деякі основні проблеми, і його використання в більшості випадків не рекомендується. Щоб отримати повну відповідь, див. Пункт 11, " Ефективна Java " Джошуа Блоха. Я вважаю, що ви можете спокійно використовувати Object.clone()масиви примітивного типу, але крім цього вам потрібно бути розумним щодо правильного використання та перекриття клону.

Схеми, які покладаються на серіалізацію (XML чи іншим чином), є химерними.

Тут немає простої відповіді. Якщо ви хочете глибоко скопіювати об'єкт, вам доведеться пройти графік об'єкта та скопіювати кожен дочірній об'єкт явно через конструктор копій об'єкта або статичний заводський метод, який у свою чергу глибоко копіює дочірній об’єкт. Незмінні Stringкопії (наприклад, s) не потрібно копіювати. Як осторонь, ви повинні віддати перевагу незмінності з цієї причини.


58

Ви можете зробити глибоку копію з серіалізацією без створення файлів.

Об'єкт, який ви бажаєте скопіювати, потрібно буде implement serializable. Якщо клас не є остаточним або його неможливо змінити, розширіть клас та застосуйте серіалізацію.

Перетворіть свій клас у потік байтів:

ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(object);
oos.flush();
oos.close();
bos.close();
byte[] byteData = bos.toByteArray();

Відновіть свій клас із потоку байтів:

ByteArrayInputStream bais = new ByteArrayInputStream(byteData);
(Object) object = (Object) new ObjectInputStream(bais).readObject();

4
Якщо клас остаточний, як би ви продовжили його?
Кумар Маніш

1
Клас @KumarManish MyContainer реалізує Serializable {екземпляр MyFinalClass; ...}
Маттео Т.

Я вважаю це чудовою відповіддю. Клон - безлад
blackbird014

@MatteoT. яким чином властивість несеризаційного класу буде серіалізуватися, не серіалізуватись instanceу цьому випадку?
Фарид

40

Ви можете зробити глибокий клон на основі серіалізації, використовуючи org.apache.commons.lang3.SerializationUtils.clone(T)Apache Commons Lang, але будьте обережні - продуктивність є безглуздою.

Загалом, найкраще застосовувати власні методи клонування для кожного класу об'єкта в графіку об'єкта, що потребує клонування.


Він також доступний уorg.apache.commons.lang.SerializationUtils
Піно

25

Один із способів реалізації глибокої копії - додавання конструкторів копій до кожного асоційованого класу. Конструктор копій бере екземпляр "цього" як єдиний аргумент і копіює з нього всі значення. Досить певної роботи, але досить прямої та безпечної.

EDIT: зауважте, що для читання полів вам не потрібно використовувати методи аксесуара. Ви можете отримати доступ до всіх полів безпосередньо, тому що вихідний екземпляр завжди того ж типу, що і екземпляр із конструктором копій. Очевидне, але може бути не помічене.

Приклад:

public class Order {

    private long number;

    public Order() {
    }

    /**
     * Copy constructor
     */
    public Order(Order source) {
        number = source.number;
    }
}


public class Customer {

    private String name;
    private List<Order> orders = new ArrayList<Order>();

    public Customer() {
    }

    /**
     * Copy constructor
     */
    public Customer(Customer source) {
        name = source.name;
        for (Order sourceOrder : source.orders) {
            orders.add(new Order(sourceOrder));
        }
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Редагувати: Зауважте, що при використанні конструкторів копій необхідно знати тип виконання об'єкта, який ви копіюєте. При наведеному вище підході ви не можете легко скопіювати змішаний список (можливо, ви зможете зробити це за допомогою коду відображення).


1
Просто зацікавлений у тому випадку, коли те, що ви копіюєте, є підкласом, але на нього посилається батьків. Чи можливо замінити конструктор копій?
Pork 'n' Bunny

Чому ваш батьківський клас посилається на його підклас? Чи можете ви навести приклад?
Адріан Костер

1
Автомобіль громадського класу поширює транспортний засіб А потім відноситься до машини як до транспортного засобу. originaList = новий ArrayList <Vehicle>; copyList = новий ArrayList <Vehicle>; originalList.add (новий автомобіль ()); for (Транспортний засіб: VehicleList) {copyList.add (новий транспортний засіб (транспортний засіб)); }
Pork 'n' Bunny

@AdriaanKoster: Якщо вихідний список містить a Toyota, ваш код буде вставлений у список Carпризначення. Правильне клонування зазвичай вимагає, щоб клас надавав метод віртуальної фабрики, у контракті якого зазначено, що він поверне новий об'єкт власного класу; сам кондуктор копії повинен бути protectedтаким, щоб він використовувався лише для побудови об'єктів, точний тип яких відповідає об'єкту, який копіюється).
supercat

Тож якщо я правильно зрозумів вашу пропозицію, заводський метод закликав би приватний конструктор копій? Як конструктор копій підкласу переконається, що поля надкласу ініціалізовані? Чи можете ви навести приклад?
Адріан Костер

20

Можна використовувати бібліотеку, яка має простий API та виконує відносно швидке клонування з відображенням (має бути швидше, ніж методи серіалізації).

Cloner cloner = new Cloner();

MyClass clone = cloner.deepClone(o);
// clone is a deep-clone of o

19

Apache commons пропонує швидкий спосіб глибокого клонування предмета.

My_Object object2= org.apache.commons.lang.SerializationUtils.clone(object1);

1
Це працює лише для об'єкта, який реалізує Serializable, а також для всіх полів, що в ньому реалізуються, хоч Serializable.
winhee

11

XStream дійсно корисний у таких випадках. Ось простий код зробити клонування

private static final XStream XSTREAM = new XStream();
...

Object newObject = XSTREAM.fromXML(XSTREAM.toXML(obj));

1
Nooo, вам не потрібні накладні витрати xml-ing об’єкта.
egelev

@egeleve Ви розумієте, що ви відповідаєте на коментар '08, правда? Я більше не використовую Java, і зараз, мабуть, є кращі інструменти. Однак у той час серіалізація до іншого формату, а потім серіалізація назад здавалася гарним злому - це було безумовно неефективно.
санкара


10

Для користувачів Spring Framework . Використання класу org.springframework.util.SerializationUtils:

@SuppressWarnings("unchecked")
public static <T extends Serializable> T clone(T object) {
     return (T) SerializationUtils.deserialize(SerializationUtils.serialize(object));
}

9

Для складних об'єктів і коли продуктивність не значна, я використовую бібліотеку json, як-от gson, щоб серіалізувати об’єкт у json-текст, а потім деріаріалізувати текст, щоб отримати новий об’єкт.

gson, який базується на відображенні, працюватиме в більшості випадків, за винятком того, що transientполя не будуть скопійовані та об'єкти з круговою посиланням із причиною StackOverflowError.

public static <T> T copy(T anObject, Class<T> classInfo) {
    Gson gson = new GsonBuilder().create();
    String text = gson.toJson(anObject);
    T newObject = gson.fromJson(text, classInfo);
    return newObject;
}
public static void main(String[] args) {
    String originalObject = "hello";
    String copiedObject = copy(originalObject, String.class);
}

3
Будь ласка, дотримуйтесь конвенцій про іменування Java для себе та заради нас.
Патрік Бергнер

8

Використовуйте XStream ( http://x-stream.github.io/ ). Ви навіть можете контролювати, які властивості можна ігнорувати за допомогою анотацій або явно вказувати ім'я властивості для класу XStream. Більше того, вам не потрібно реалізовувати інтерфейс, що може бути закритим.


7

Глибоке копіювання можна зробити лише за згодою кожного класу. Якщо у вас є контроль над ієрархією класів, ви можете реалізувати інтерфейс, що може бути закритий, та застосувати метод Clone. В іншому випадку зробити глибоку копію неможливо зробити безпечно, оскільки об’єкт може також обмінюватися ресурсами, що не належать до даних (наприклад, підключення до бази даних). Як правило, проте глибоке копіювання вважається поганою практикою в середовищі Java, і цього слід уникати за допомогою відповідної практики проектування.


2
Не могли б ви описати "відповідну практику проектування"?
fklappan

6
import com.thoughtworks.xstream.XStream;

public class deepCopy {
    private static  XStream xstream = new XStream();

    //serialize with Xstream them deserialize ...
    public static Object deepCopy(Object obj){
        return xstream.fromXML(xstream.toXML(obj));
    }
}

5

Я використовував Dozer для клонування об'єктів Java, і це чудово, бібліотека Kryo - ще одна чудова альтернатива.



2

1)

public static Object deepClone(Object object) {
   try {
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     ObjectOutputStream oos = new ObjectOutputStream(baos);
     oos.writeObject(object);
     ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
     ObjectInputStream ois = new ObjectInputStream(bais);
     return ois.readObject();
   }
   catch (Exception e) {
     e.printStackTrace();
     return null;
   }
 }

2)

    // (1) create a MyPerson object named Al
    MyAddress address = new MyAddress("Vishrantwadi ", "Pune", "India");
    MyPerson al = new MyPerson("Al", "Arun", address);

    // (2) make a deep clone of Al
    MyPerson neighbor = (MyPerson)deepClone(al);

Тут ваш клас MyPerson і MyAddress повинен реалізовувати інтерфейс, який можна засвоювати


2

Використання Джексона для серіалізації та десеріалізації об'єкта. Ця реалізація не вимагає від об'єкта реалізації класу Serializable.

  <T> T clone(T object, Class<T> clazzType) throws IOException {

    final ObjectMapper objMapper = new ObjectMapper();
    String jsonStr= objMapper.writeValueAsString(object);

    return objMapper.readValue(jsonStr, clazzType);

  }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.