Как правильно клонировать объект?

Для клонирования объекта в Java можно пользоваться тремя способами:
  1. Переопределение метода clone() и реализация интерфейса Cloneable();
  2. Использование конструктора копирования;
  3. Использовать для клонирования механизм сериализации

Теперь по порядку. Первый способ подразумевает, что вы будете использовать механизм так называемого «поверхностного клонирования» и сами позаботитесь о клонировании полей-объектов. Метод clone() в родительском классе Object является protected, поэтому требуется переопределение его с объявлением как public. Он возвращает экземпляр объекта с копированными полями-примитивами и ссылками. И получается что у оригинала и его клона поля-ссылки указывают на одни и те же объекты. Пример далее показывает, как одновременно меняется поле у оригинального объекта и клона.

public class CloneTest{
    static class Person implements Cloneable{
        String name;
        int age;
        Car car;
        Person(Car car,int age,String name) {
            this.car = car;
            this.age = age;
            this.name = name;
        }

        @Override
        public String toString() {
            return this.name+" {" +
                    "age=" + age +
                    ", car=" + car +
                    '}';
        }

        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
    static class Car{
        public String color;

        Car(String color) {
            this.color = color;
        }

        @Override
        public String toString() {
            return "{" +
                    "color car='" + color + '\'' +
                    '}';
        }
    }


    public static void main(String[] args) throws CloneNotSupportedException {
        Car car = new Car("Green");
        Person person=new Person(car,25,"Mike");

        Person clone = (Person) person.clone();
        System.out.println(person);
        System.out.println(clone);
        clone.name=new String("Ivan");
        clone.car.color="red";
        System.out.println(person);
        System.out.println(clone);
    }
}
	Вывод:
    	Mike {age=25, car={color car='Green'}}
	Mike {age=25, car={color car='Green'}}
	Mike {age=25, car={color car='red'}}
	Ivan {age=25, car={color car='red'}}

Из примера выше видно, что у клона и оригинала состояние одного из полей изменяется одновременно. Следующий способ заключается в использовании конструктора копирования:

public class Person {
        private int age;
        private String name;
        public Person(int age, String name){
            this.age=age;
            this.name=name;
        }
        // конструктор копии
        public Person(Person other) {
            this(other.getAge(), other.getName());
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        public String getName() {
            return name;
        }

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

        @Override
        public String toString() {
            return "Person{" +
                    "age=" + age +
                    ", name='" + name + '\'' +
                    '}';
        }

        public static void main(String[] args) {
            Person original = new Person(18, "Grishka");
            Person clone = new Person(original);
            System.out.println(original);
            System.out.println(clone);
        }
}
	Вывод:
	Person{age=18, name='Grishka'}
	Person{age=18, name='Grishka'}

В классе описывается конструктор, который принимает объект этого же класса и инициализирует значениями его полей поля нового объекта. О реализации инициализации полей полностью должен позаботиться разработчик класса.

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

import java.io.*;

class Cat implements Serializable{
    private String name;
    private String color;
    private int age;

    public Cat(String name, String color, int age) {
        this.name = name;
        this.color = color;
        this.age = age;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                ", color='" + color + '\'' +
                ", age=" + age +
                '}';
    }
}
public class BasketCats{
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Cat vaska = new Cat("Vaska","Gray",4);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream ous = new ObjectOutputStream(baos);
        //сохраняем состояние кота Васьки в поток и закрываем его(поток)
        ous.writeObject(vaska);
        ous.close();
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        //создаём кота для опытов и инициализируем его состояние Васькиным
        Cat cloneVaska = (Cat)ois.readObject();
        System.out.println(vaska);
        System.out.println(cloneVaska);
        System.out.println("*********************************************");
        cloneVaska.setColor("Black");
        //Убеждаемся что у кота Васьки теперь есть клон, над которым можно ставить опыты без ущерба Василию
        System.out.println(vaska);
        System.out.println(cloneVaska);

    }
}
	Вывод:
    	Cat{name='Vaska', color='Gray', age=4}
	Cat{name='Vaska', color='Gray', age=4}
	*********************************************
	Cat{name='Vaska', color='Gray', age=4}
	Cat{name='Vaska', color='Black', age=4}

Ни один кот не пострадал в результате тестов, мы видим что Васька был сохранён в поток, из которого затем восстановили независимый клон. Если нет особой необходимости обработки полей во время клонирования объектов, то сериализация является наиболее предпочтительным вариантом для этих целей.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 14

    +6
    На сколько мне известно, клонирование — это создание нового идентичного экземпляра класса с выделением памяти под вложенные экземпляры других классов или типов данных. В случае с клонированием иерархии инкапсулированных классов — все они должны быть clonable, и это должен решать разработчик когда он хочет реализовывать полноценное клонирование. Тоже самое и с конструкторами копирования — за этим просто нужно следить, и к этому просто нужно привыкнуть. Это не плохие решения, потому что они ведут к довольно простым ошибкам и требуют внимательности, это просто разработчики сейчас слишком ленивые пошли.

    Если в серьёзной конторе встречается «эникей», который не может оперировать этими особенностями, которые кстати тоже характерны и для С++, то отдельно пишется набор рефлексивных тестов, которые проверяют возможность клонирования классов в приложении, чтобы это не приводило к странным последствиям и непредсказуемому поведению.

    Я не думаю то сериализация-десерелиализация хорошее решение — там слишком много накладных машинных расходов на всякие выравнивания, memcpy syscall'ы и прочее. В серьёзных проектах это просто неприемлемо.
      0
      Да, хотел ещё добавить что эти вещи достаточно просто реализовать посредством сторонних манипуляций с байткодом в рамках того же Project Graal, но это тема отдельной статьи.
      +3
      Забыли для какого-нибудь вложенного объекта implements Serializable — привет.
        +1
        Но оба вышеуказанных способа полны потенциальных ошибок и по сути создают копию объекта. Наиболее удобным и гибким способом клонирования является механизм сериализации. Он заключается в сохранении объекта в поток байтов с последующей эксгумацией его от туда. Для примера пригласим кота Ваську, его ждёт пара опытов:

        ИМХО, но
        1) Вы не указали, что в случае с клонированием можно реализовать глубокое клонирование вручную, например, в 1 примере для этого нужно было сделать класс Car тоже клонируемым и вызвать у него clone, тогда класс Person будет содержать не ссылки, а новые объекты. Новички в Java могут решить что клонирование действительно может быть только поверхностным,
        2) Сериализация тоже не серебряная пуля, у неё хватает своих проблем, достаточно забыть serialVersionUID и эволюция классов в продакшене станет проблематичной,
        3) На самом деле, клонирование с покрытием его правильным unit-test'ом выглядит намного правильнее, чем использование сериализации. Более того, интерфейс клонирования будет понятен любому Java программисту (и частично IDE), а вот использование сериализации только для получения копии объекта скорее всего вынесет мозг стороннему разработчику (он легко может решить что происходит ошибка увидев одновременную сериализацию и десериализацию вместе).
          +3
          Автор, а вы Джошуа Блоха читали? Он в «Effective Java» рассматривал способы клонирования (Item 11). В частности, предложенный способ с помощью конструктора копирования или (не указанный Вами) static метод копирования указывается им как лучшее на данный момент решение. Из плюсов в нём — нет ошибок из за наследования (например, не нужно беспокоится, что у наследников появятся новые поля и через метод clone() они не скопируются), явное указание клонированных полей с возможность клонировать final поля (например, есть final поле, которое ссылается на объект, и в конструкторе копирования делается копия этого объекта и присваивается final полю в клон).
          А на счёт сериализации выше правильно написали — минусов этого решения очень много и вообще предназначено оно не для этого.
            +2
            «нет ошибок из за наследования. (например, не нужно беспокоится, что у наследников появятся новые поля и через метод clone() они не скопируются)»

            Я не пойму о чем речь, о каких ошибках из за наследования идет речь? Можете привети пример, воспроизводящий это? Object.clone() копирует же все поля конечного обьекта.
              +1
              Ну, смотрите, есть супер класс SuperClass c полями Class1 cls1 и Class2 cls2. Мы в наследнике SuperClass конечно можем переопределить клонирование этих полей, чтобы сохранять в них новые объекты, а не ссылки, однако если в будущем в SuperClass добавятся поля Class3 cls3 и Class4 cls4, то наше глубокое клонирование внезапно сломается, так как cls3 и cls4 будут у клона и источника содержать ссылки на одинаковые объекты. Хуже всего, что компилятор нам ничего не скажет и даже ошибки в рантайме не будет, просто логика работы класса скорее всего будет неправильная.
                +2
                Ваша мысль ясна, но насколько я понял, вы ее объяснили не теми словами.

                Насколько я понял, суть вашего тезиса в том, что ломается не клонирование, а ломается его глубокость, если мы забываем клонировать новые поля. Правильно?

                Но если так — то давайте рассматривать и static-метод в условиях, когда мы забыли клонировать новые поля. В этом случае, новые поля будут равны null.

                Раз так, наверное, надо рассматривать глубокое и неглубокое как два разных клонирования. Через clone() лучше делать неглубокое клонирование, а глубокое — в зависимости от задач другими методами.
                  0
                  Насколько я понял, суть вашего тезиса в том, что ломается не клонирование, а ломается его глубокость, если мы забываем клонировать новые поля. Правильно?

                  Правильно.

                  Но если так — то давайте рассматривать и static-метод в условиях, когда мы забыли клонировать новые поля. В этом случае, новые поля будут равны null.

                  Не обязательно, если static-метод использует конструктор, то ошибку мы получим ещё на этапе компиляции — добавление новых полей, как правило, вызовет расширение конструктора и ошибку компиляции в static-методе.

                  Через clone() лучше делать неглубокое клонирование, а глубокое — в зависимости от задач другими методами.

                  Не обязательно, например если ставит правило что каждый класс в иерархии должен обязательно иметь свой переопределенный clone(), тогда большинство проблем можно решить просто используя super.clone().
                  +1
                  Точно так же можно забыть глубоко склонировать новое поле в конструкторе копирования. Шансов меньше, но всё-таки.

                  Самое идейно правильное клонирование — это использовать только неизменяемые объекты, тогда оно в принципе будет не нужно :-) Конечно, в отдельных случаях могут быть сложности, но в примерах автора этой статьи неизменяемые объекты просто напрашиваются.
              +1
              А как же фабричный метод? Он вроде как дешевле сериализации, безопаснее clone'а и толерантен к полиморфизму.
                0
                Ну, фабричный метод обычно не создает копию конкретного объекта с конкретным состоянием, то есть вряд ли его можно назвать методом клонирования (хотя зависит от ситуации, конечно).
                +2
                Предложенное решение весьма плохо в силу своей негибкости. К примеру, вам будет очень сложно переиспользовать существующие в системе объекты, которые клонировать не надо или которые неизменяемы и клонировать их не имеет смысла. Плюс сериализация — это дополнительные ненужные затраты памяти.

                Если реализуете clone, имеет смысл заменить возвращаемый тип метода на ковариантный.
                  +1
                  А впрочем держите новогодний плюсик в карму. Попытка была забавной :-) Успехов вам!

                Only users with full accounts can post comments. Log in, please.